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 */ 014 015package ch.qos.logback.core.model.processor; 016 017import ch.qos.logback.core.Context; 018import ch.qos.logback.core.FileAppender; 019import ch.qos.logback.core.model.AppenderModel; 020import ch.qos.logback.core.model.ImplicitModel; 021import ch.qos.logback.core.model.Model; 022import ch.qos.logback.core.model.SiftModel; 023import ch.qos.logback.core.rolling.RollingFileAppender; 024import ch.qos.logback.core.rolling.helper.FileNamePattern; 025 026import java.util.ArrayList; 027import java.util.HashMap; 028import java.util.List; 029import java.util.Map; 030import java.util.Optional; 031import java.util.function.Supplier; 032import java.util.stream.Collectors; 033import java.util.stream.Stream; 034 035@PhaseIndicator(phase = ProcessingPhase.DEPENDENCY_ANALYSIS) 036public class FileCollisionAnalyser extends ModelHandlerBase { 037 038 public FileCollisionAnalyser(Context context) { 039 super(context); 040 } 041 042 @Override 043 protected Class<AppenderModel> getSupportedModelClass() { 044 return AppenderModel.class; 045 } 046 047 048 @Override 049 public void handle(ModelInterpretationContext mic, Model model) throws ModelHandlerException { 050 AppenderModel appenderModel = (AppenderModel) model; 051 052 String originalClassName = appenderModel.getClassName(); 053 String className = mic.getImport(originalClassName); 054 055 String appenderName = appenderModel.getName(); 056 057 // A SiftingAppender instantiates its nested appender once per discriminator 058 // value at runtime, so the nested appender escapes the static collision maps 059 // handled below. If its file/fileNamePattern does not reference the 060 // discriminator key, every instance resolves to the same target and their 061 // output silently collides. Detect that at model-analysis time (see #1041). 062 Model siftModel = firstSubModelOfClass(appenderModel, SiftModel.class); 063 if (siftModel != null) { 064 checkSiftingAppenderForCollision(mic, appenderModel, siftModel, appenderName); 065 return; 066 } 067 068 if (!fileAppenderOrRollingFileAppender(className)) { 069 return; 070 } 071 072 String tagName0 = "file"; 073 checkForCollisions(mic, MapKey.FILE_COLLISION_MAP_KEY, appenderModel, appenderName, tagName0); 074 075 String tagName1 = "fileNamePattern"; 076 checkForCollisions(mic, MapKey.RFA_FILENAME_COLLISION_MAP, appenderModel, appenderName, tagName1); 077 } 078 079 private static boolean fileAppenderOrRollingFileAppender(String className) { 080 return FileAppender.class.getName().equals(className) || RollingFileAppender.class.getName().equals(className); 081 } 082 083 084 boolean tagPredicate(Model model, String tagName) { 085 return (model instanceof ImplicitModel) && tagName.equals(model.getTag()); 086 } 087 088 enum MapKey { 089 FILE_COLLISION_MAP_KEY, RFA_FILENAME_COLLISION_MAP 090 } 091 092 private void checkForCollisions(ModelInterpretationContext mic, MapKey mapKey, AppenderModel appenderModel, String appenderName, final String tagName) { 093 094 095 Stream<Model> streamLevel1 = appenderModel.getSubModels().stream(); 096 Stream<Model> streamLevel2 = appenderModel.getSubModels().stream().flatMap(child -> child.getSubModels().stream()); 097 098 List<Model> matchingModels = Stream.concat(streamLevel1, streamLevel2).filter(m -> tagPredicate(m, tagName)).collect(Collectors.toList()); 099 100 if(!matchingModels.isEmpty()) { 101 ImplicitModel implicitModel = (ImplicitModel) matchingModels.get(0); 102 String bodyValue = mic.subst(implicitModel.getBodyText()); 103 104 105 Map<String, String> collisionMap = getCollisionMapByKey(mic, mapKey); 106 107 Optional<Map.Entry<String, String>> collision = collisionMap.entrySet() 108 .stream() 109 .filter(entry -> bodyValue.equals(entry.getValue())) 110 .findFirst(); 111 112 if (collision.isPresent()) { 113 addErrorForCollision(tagName, appenderName, collision.get().getKey(), bodyValue); 114 appenderModel.markAsHandled(); 115 appenderModel.deepMarkAsSkipped(); 116 } else { 117 // add to collision map if and only if no collision detected 118 // reasoning: single entry is as effective as multiple entries for collision detection 119 collisionMap.put(appenderName, bodyValue); 120 } 121 } 122 } 123 124 private Map<String, String> getCollisionMapByKey(ModelInterpretationContext mic, MapKey mapKey) { 125 Map<String, String> map = (Map<String, String>) mic.getObjectMap().get(mapKey.name()); 126 if(map == null) { 127 map = new HashMap<>(); 128 mic.getObjectMap().put(mapKey.name(), map); 129 } 130 return map; 131 } 132 133 134 static public final String COLLISION_DETECTED = "Collision detected. Skipping initialization of appender named [%s]"; 135 static public final String COLLISION_MESSAGE = "In appender [%s] option '%s' has the same value '%s' as that set for appender [%s] defined earlier"; 136 private void addErrorForCollision(String optionName, String appenderName, String previousAppenderName, String optionValue) { 137 addError(String.format(COLLISION_DETECTED, appenderName)); 138 addError(String.format(COLLISION_MESSAGE, appenderName, optionName, optionValue, previousAppenderName)); 139 } 140 141 static public final String SIFT_COLLISION_MESSAGE_0 = 142 "The nested appender of SiftingAppender [%s] does not reference the discriminator key [%s] in its 'file'/'fileNamePattern'. "; 143 static public final String SIFT_COLLISION_MESSAGE_1 = 144 "Every child appender will therefore write to the same file [%s] causing collisions. "; 145 static public final String SIFT_COLLISION_MESSAGE_2 = "Consider embedding ${%s} in the file path."; 146 147 /** 148 * Detect the SiftingAppender variant of a file collision: the nested appender is 149 * created once per discriminator value, so unless its file/fileNamePattern embeds a 150 * reference to the discriminator key, all instances resolve to the same target. 151 */ 152 private void checkSiftingAppenderForCollision(ModelInterpretationContext mic, AppenderModel appenderModel, 153 Model siftModel, String appenderName) { 154 155 String discriminatorKey = findDiscriminatorKey(appenderModel); 156 if (discriminatorKey == null || discriminatorKey.isEmpty()) { 157 // discriminator key not declared in XML (e.g. a discriminator with a built-in 158 // default key); can't reason about the file pattern, so stay silent. 159 return; 160 } 161 162 Model nestedAppenderModel = firstSubModelOfClass(siftModel, AppenderModel.class); 163 if (nestedAppenderModel == null) { 164 return; 165 } 166 167 List<String> fileValues = new ArrayList<>(); 168 collectBodyText(nestedAppenderModel, "file", fileValues); 169 collectBodyText(nestedAppenderModel, "fileNamePattern", fileValues); 170 171 if (fileValues.isEmpty()) { 172 // nested appender writes to no file (e.g. ConsoleAppender); nothing to collide. 173 return; 174 } 175 176 String keyReference = "${" + discriminatorKey; 177 boolean referencesKey = fileValues.stream().anyMatch(v -> v.contains(keyReference)); 178 if (!referencesKey) { 179 String resolvedTarget = mic.subst(fileValues.get(0)); 180 addWarn(String.format(SIFT_COLLISION_MESSAGE_0, appenderName, discriminatorKey)); 181 addWarn(String.format(SIFT_COLLISION_MESSAGE_1, resolvedTarget)); 182 addWarn(String.format(SIFT_COLLISION_MESSAGE_2, discriminatorKey)); 183 } 184 } 185 186 private Model firstSubModelOfClass(Model parent, Class<? extends Model> modelClass) { 187 return parent.getSubModels().stream().filter(modelClass::isInstance).findFirst().orElse(null); 188 } 189 190 private String findDiscriminatorKey(AppenderModel appenderModel) { 191 Optional<Model> discriminator = appenderModel.getSubModels().stream() 192 .filter(m -> "discriminator".equalsIgnoreCase(m.getTag())).findFirst(); 193 194 if (discriminator.isEmpty()) { 195 return null; 196 } 197 198 for (Model child : discriminator.get().getSubModels()) { 199 if ("key".equalsIgnoreCase(child.getTag())) { 200 String bodyText = child.getBodyText(); 201 if (bodyText != null) { 202 return bodyText.trim(); 203 } 204 } 205 } 206 return null; 207 } 208 209 /** 210 * Collect the raw body text of every {@code <tagName>} element nested (one or two levels 211 * deep) inside the given appender model. Raw text is used on purpose: the discriminator 212 * key is only bound as a substitution property at runtime, so it must be matched textually. 213 */ 214 private void collectBodyText(Model appenderModel, String tagName, List<String> out) { 215 for (Model child : appenderModel.getSubModels()) { 216 // level 1: direct children of the appender (e.g. <file>) 217 addBodyTextIfMatching(child, tagName, out); 218 // level 2: grandchildren (e.g. <fileNamePattern> under a rolling policy) 219 for (Model grandChild : child.getSubModels()) { 220 addBodyTextIfMatching(grandChild, tagName, out); 221 } 222 } 223 } 224 225 private void addBodyTextIfMatching(Model model, String tagName, List<String> out) { 226 if (!tagPredicate(model, tagName)) { 227 return; 228 } 229 String bodyText = model.getBodyText(); 230 if (bodyText != null) { 231 out.add(bodyText); 232 } 233 } 234}