View Javadoc
1   /*
2    * Logback: the reliable, generic, fast and flexible logging framework.
3    * Copyright (C) 1999-2026, QOS.ch. All rights reserved.
4    *
5    * This program and the accompanying materials are dual-licensed under
6    * either the terms of the Eclipse Public License v2.0 as published by
7    * the Eclipse Foundation
8    *
9    *   or (per the licensee's choosing)
10   *
11   * under the terms of the GNU Lesser General Public License version 2.1
12   * as published by the Free Software Foundation.
13   */
14  package ch.qos.logback.classic.model.processor;
15  
16  import java.util.LinkedHashMap;
17  import java.util.LinkedHashSet;
18  import java.util.Map;
19  import java.util.Set;
20  import java.util.regex.Pattern;
21  
22  import ch.qos.logback.classic.AsyncAppender;
23  import ch.qos.logback.classic.net.SMTPAppender;
24  import ch.qos.logback.classic.net.SocketAppender;
25  import ch.qos.logback.core.Context;
26  import ch.qos.logback.core.model.AppenderModel;
27  import ch.qos.logback.core.model.ImplicitModel;
28  import ch.qos.logback.core.model.Model;
29  import ch.qos.logback.core.model.processor.ModelHandlerBase;
30  import ch.qos.logback.core.model.processor.ModelHandlerException;
31  import ch.qos.logback.core.model.processor.ModelInterpretationContext;
32  import ch.qos.logback.core.model.processor.PhaseIndicator;
33  import ch.qos.logback.core.model.processor.ProcessingPhase;
34  import ch.qos.logback.core.util.OptionHelper;
35  
36  /**
37   * Dependency-analysis pass over every {@link AppenderModel}: records which
38   * appenders suppress caller data ({@link AsyncAppender}, {@link SocketAppender}
39   * or {@link SMTPAppender} with {@code includeCallerData=false} / default) and
40   * which appenders need it (pattern contains a caller-data converter).
41   *
42   * <p>{@link SMTPAppender} is special: it both preprocesses caller data
43   * ({@code includeCallerData}) and formats events via its own layout. Those two
44   * contributions are recorded as separate map entries under
45   * {@code name + }{@link #INCLUDE_CALLER_DATA_NAME_SUFFIX} and
46   * {@code name + }{@link #LAYOUT_NAME_SUFFIX} so that contradictions within a
47   * single SMTP appender (e.g. includeCallerData=false but layout uses
48   * {@code %C}) can be detected.</p>
49   *
50   * <p>Analysis is skipped when the variable
51   * {@value #SKIP_CALLER_CONTRADICTION_ANALYSIS_PROPERTY} is set to
52   * {@code true} (context property, local property, system property or
53   * environment variable).</p>
54   *
55   * <p>The contradiction check is performed by {@link CallerContradictionWarnAnalyser}
56   * in its {@code postHandle()} on the enclosing {@code ConfigurationModel}, after
57   * all appender models have been visited.</p>
58   *
59   * @since 1.6.2
60   * @see CallerContradictionWarnAnalyser
61   */
62  @PhaseIndicator(phase = ProcessingPhase.DEPENDENCY_ANALYSIS)
63  public class CallerContradictionAnalyser extends ModelHandlerBase {
64  
65      static final String APPENDER_TO_CALLER_INSTRUCTION_MAP_KEY = "APPENDER_TO_CALLER_INSTRUCTION_MAP_KEY";
66  
67      /**
68       * When this property/variable is {@code true}, caller-contradiction analysis
69       * is not performed.
70       */
71      public static final String SKIP_CALLER_CONTRADICTION_ANALYSIS_PROPERTY =
72              "logback.skipCallerContradictionAnalysis";
73  
74      /**
75       * Map-key suffix for an SMTPAppender {@code includeCallerData} contribution.
76       */
77      static final String INCLUDE_CALLER_DATA_NAME_SUFFIX = ".includeCallerData";
78  
79      /**
80       * Map-key suffix for an SMTPAppender layout pattern contribution.
81       */
82      static final String LAYOUT_NAME_SUFFIX = ".layout";
83  
84      /**
85       * Matches caller-data converter words in a logback pattern string.
86       * Single-char forms (%C class, %M method, %L line, %F file, %l location) are
87       * case-sensitive; multi-char aliases (class, method, line, file, caller) are
88       * case-insensitive and unique enough to match without case sensitivity issues.
89       * Negative lookahead prevents partial matches like %Msg being flagged.
90       */
91      static final Pattern CALLER_PATTERN = Pattern.compile(
92              "%([CMLFl]|caller|class|method|line|file)(?![a-zA-Z])");
93  
94      public CallerContradictionAnalyser(Context context) {
95          super(context);
96      }
97  
98      @Override
99      protected Class<AppenderModel> getSupportedModelClass() {
100         return AppenderModel.class;
101     }
102 
103     @Override
104     public void handle(ModelInterpretationContext mic, Model model) throws ModelHandlerException {
105         if (isSkipCallerContradictionAnalysis(mic)) {
106             return;
107         }
108 
109         AppenderModel appenderModel = (AppenderModel) model;
110 
111         Map<String, CallerInstructionLogic.Instruction> appenderNameToCallerInstructionMap
112                 = getAppenderNameToCallerInstructionMap(mic);
113 
114         String originalClassName = appenderModel.getClassName();
115         String className = mic.getImport(originalClassName);
116         String appenderName = mic.subst(appenderModel.getName());
117 
118         if (SMTPAppender.class.getName().equals(className)) {
119             recordSmtpAppenderInstructions(mic, appenderModel, appenderName,
120                     appenderNameToCallerInstructionMap);
121             return;
122         }
123 
124         if (isCallerDataPreprocessingAppender(className)) {
125             if (isIncludeCallerDataTrue(mic, appenderModel)) {
126                 appenderNameToCallerInstructionMap.put(appenderName,
127                         CallerInstructionLogic.Instruction.PREPROCESS_WANT);
128             } else {
129                 appenderNameToCallerInstructionMap.put(appenderName,
130                         CallerInstructionLogic.Instruction.DO_NOT_WANT);
131             }
132         }
133 
134         if (hasCallerDataConverters(appenderModel)) {
135             appenderNameToCallerInstructionMap.put(appenderName, CallerInstructionLogic.Instruction.DIRECT_WANT);
136         }
137     }
138 
139     /**
140      * Returns {@code true} when {@value #SKIP_CALLER_CONTRADICTION_ANALYSIS_PROPERTY}
141      * resolves to {@code true}. Lookup order is local interpretation properties,
142      * context properties, system properties, then environment variables.
143      */
144     static boolean isSkipCallerContradictionAnalysis(ModelInterpretationContext mic) {
145         String value = OptionHelper.propertyLookup(SKIP_CALLER_CONTRADICTION_ANALYSIS_PROPERTY, mic,
146                 mic.getContext());
147         return OptionHelper.toBoolean(value, false);
148     }
149 
150     /**
151      * Records SMTPAppender contributions as two distinct instructions so that
152      * {@code includeCallerData} and the layout pattern can contradict each other.
153      * <p>
154      * The subject pattern is intentionally ignored; only the layout subtree is
155      * considered for {@link CallerInstructionLogic.Instruction#DIRECT_WANT}.
156      * </p>
157      */
158     private void recordSmtpAppenderInstructions(ModelInterpretationContext mic, AppenderModel appenderModel,
159             String appenderName, Map<String, CallerInstructionLogic.Instruction> map) {
160         String includeCallerDataKey = appenderName + INCLUDE_CALLER_DATA_NAME_SUFFIX;
161         if (isIncludeCallerDataTrue(mic, appenderModel)) {
162             map.put(includeCallerDataKey, CallerInstructionLogic.Instruction.PREPROCESS_WANT);
163         } else {
164             map.put(includeCallerDataKey, CallerInstructionLogic.Instruction.DO_NOT_WANT);
165         }
166 
167         // Subject is not scanned — only layout patterns contribute DIRECT_WANT.
168         Model layoutModel = findLayoutSubModel(appenderModel);
169         if (layoutModel != null && hasCallerDataConvertersIn(layoutModel)) {
170             map.put(appenderName + LAYOUT_NAME_SUFFIX, CallerInstructionLogic.Instruction.DIRECT_WANT);
171         }
172     }
173 
174     private Model findLayoutSubModel(AppenderModel appenderModel) {
175         for (Model child : appenderModel.getSubModels()) {
176             if ("layout".equalsIgnoreCase(child.getTag())) {
177                 return child;
178             }
179         }
180         return null;
181     }
182 
183     /**
184      * Appenders that optionally extract caller data before deferred processing
185      * or serialization, controlled by the {@code includeCallerData} property
186      * (default {@code false}). {@link SMTPAppender} is handled separately.
187      */
188     private boolean isCallerDataPreprocessingAppender(String className) {
189         return AsyncAppender.class.getName().equals(className)
190                 || SocketAppender.class.getName().equals(className);
191     }
192 
193     /**
194      * Note that includeCallerData is false by default on AsyncAppender,
195      * SocketAppender and SMTPAppender, so if the tag is absent we treat it as
196      * false.
197      *
198      * @param mic
199      * @param appenderModel
200      * @return
201      */
202     private boolean isIncludeCallerDataTrue(ModelInterpretationContext mic,
203             AppenderModel appenderModel) {
204         for (Model child : appenderModel.getSubModels()) {
205             if (child instanceof ImplicitModel
206                     && "includeCallerData".equalsIgnoreCase(child.getTag())) {
207                 String value = mic.subst(((ImplicitModel) child).getBodyText());
208                 return "true".equalsIgnoreCase(value);
209             }
210         }
211         return false; // absent → default false
212     }
213 
214     private boolean hasCallerDataConverters(AppenderModel appenderModel) {
215         return hasCallerDataConvertersIn(appenderModel);
216     }
217 
218     private boolean hasCallerDataConvertersIn(Model model) {
219         return collectPatternBodyTexts(model).stream()
220                 .anyMatch(p -> CALLER_PATTERN.matcher(p).find());
221     }
222 
223     private Set<String> collectPatternBodyTexts(Model model) {
224         Set<String> patterns = new LinkedHashSet<>();
225         collectPatternBodyTextsRecursive(model, patterns);
226         return patterns;
227     }
228 
229     private void collectPatternBodyTextsRecursive(Model model, Set<String> out) {
230         if (model instanceof ImplicitModel && "pattern".equalsIgnoreCase(model.getTag())) {
231             String body = model.getBodyText();
232             if (body != null) {
233                 out.add(body);
234             }
235         }
236         for (Model child : model.getSubModels()) {
237             collectPatternBodyTextsRecursive(child, out);
238         }
239     }
240 
241     @SuppressWarnings("unchecked")
242     static Map<String, CallerInstructionLogic.Instruction> getAppenderNameToCallerInstructionMap(ModelInterpretationContext mic) {
243         Map<String, CallerInstructionLogic.Instruction> map =
244                 (Map<String, CallerInstructionLogic.Instruction>) mic.getObjectMap().get(APPENDER_TO_CALLER_INSTRUCTION_MAP_KEY);
245         if (map == null) {
246             map = new LinkedHashMap<>();
247             mic.getObjectMap().put(APPENDER_TO_CALLER_INSTRUCTION_MAP_KEY, map);
248         }
249         return map;
250     }
251 }