001/* 002 * Logback: the reliable, generic, fast and flexible logging framework. 003 * Copyright (C) 1999-2026, QOS.ch. All rights reserved. 004 * 005 * This program and the accompanying materials are dual-licensed under 006 * either the terms of the Eclipse Public License v2.0 as published by 007 * the Eclipse Foundation 008 * 009 * or (per the licensee's choosing) 010 * 011 * under the terms of the GNU Lesser General Public License version 2.1 012 * as published by the Free Software Foundation. 013 */ 014package ch.qos.logback.classic.model.processor; 015 016import java.util.LinkedHashMap; 017import java.util.LinkedHashSet; 018import java.util.Map; 019import java.util.Set; 020import java.util.regex.Pattern; 021 022import ch.qos.logback.classic.AsyncAppender; 023import ch.qos.logback.classic.net.SMTPAppender; 024import ch.qos.logback.classic.net.SocketAppender; 025import ch.qos.logback.core.Context; 026import ch.qos.logback.core.model.AppenderModel; 027import ch.qos.logback.core.model.ImplicitModel; 028import ch.qos.logback.core.model.Model; 029import ch.qos.logback.core.model.processor.ModelHandlerBase; 030import ch.qos.logback.core.model.processor.ModelHandlerException; 031import ch.qos.logback.core.model.processor.ModelInterpretationContext; 032import ch.qos.logback.core.model.processor.PhaseIndicator; 033import ch.qos.logback.core.model.processor.ProcessingPhase; 034import ch.qos.logback.core.util.OptionHelper; 035 036/** 037 * Dependency-analysis pass over every {@link AppenderModel}: records which 038 * appenders suppress caller data ({@link AsyncAppender}, {@link SocketAppender} 039 * or {@link SMTPAppender} with {@code includeCallerData=false} / default) and 040 * which appenders need it (pattern contains a caller-data converter). 041 * 042 * <p>{@link SMTPAppender} is special: it both preprocesses caller data 043 * ({@code includeCallerData}) and formats events via its own layout. Those two 044 * contributions are recorded as separate map entries under 045 * {@code name + }{@link #INCLUDE_CALLER_DATA_NAME_SUFFIX} and 046 * {@code name + }{@link #LAYOUT_NAME_SUFFIX} so that contradictions within a 047 * single SMTP appender (e.g. includeCallerData=false but layout uses 048 * {@code %C}) can be detected.</p> 049 * 050 * <p>Analysis is skipped when the variable 051 * {@value #SKIP_CALLER_CONTRADICTION_ANALYSIS_PROPERTY} is set to 052 * {@code true} (context property, local property, system property or 053 * environment variable).</p> 054 * 055 * <p>The contradiction check is performed by {@link CallerContradictionWarnAnalyser} 056 * in its {@code postHandle()} on the enclosing {@code ConfigurationModel}, after 057 * all appender models have been visited.</p> 058 * 059 * @since 1.6.2 060 * @see CallerContradictionWarnAnalyser 061 */ 062@PhaseIndicator(phase = ProcessingPhase.DEPENDENCY_ANALYSIS) 063public class CallerContradictionAnalyser extends ModelHandlerBase { 064 065 static final String APPENDER_TO_CALLER_INSTRUCTION_MAP_KEY = "APPENDER_TO_CALLER_INSTRUCTION_MAP_KEY"; 066 067 /** 068 * When this property/variable is {@code true}, caller-contradiction analysis 069 * is not performed. 070 */ 071 public static final String SKIP_CALLER_CONTRADICTION_ANALYSIS_PROPERTY = 072 "logback.skipCallerContradictionAnalysis"; 073 074 /** 075 * Map-key suffix for an SMTPAppender {@code includeCallerData} contribution. 076 */ 077 static final String INCLUDE_CALLER_DATA_NAME_SUFFIX = ".includeCallerData"; 078 079 /** 080 * Map-key suffix for an SMTPAppender layout pattern contribution. 081 */ 082 static final String LAYOUT_NAME_SUFFIX = ".layout"; 083 084 /** 085 * Matches caller-data converter words in a logback pattern string. 086 * Single-char forms (%C class, %M method, %L line, %F file, %l location) are 087 * case-sensitive; multi-char aliases (class, method, line, file, caller) are 088 * case-insensitive and unique enough to match without case sensitivity issues. 089 * Negative lookahead prevents partial matches like %Msg being flagged. 090 */ 091 static final Pattern CALLER_PATTERN = Pattern.compile( 092 "%([CMLFl]|caller|class|method|line|file)(?![a-zA-Z])"); 093 094 public CallerContradictionAnalyser(Context context) { 095 super(context); 096 } 097 098 @Override 099 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}