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  
15  package ch.qos.logback.core.model.processor;
16  
17  import ch.qos.logback.core.Context;
18  import ch.qos.logback.core.FileAppender;
19  import ch.qos.logback.core.model.AppenderModel;
20  import ch.qos.logback.core.model.ImplicitModel;
21  import ch.qos.logback.core.model.Model;
22  import ch.qos.logback.core.model.SiftModel;
23  import ch.qos.logback.core.rolling.RollingFileAppender;
24  import ch.qos.logback.core.rolling.helper.FileNamePattern;
25  
26  import java.util.ArrayList;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Optional;
31  import java.util.function.Supplier;
32  import java.util.stream.Collectors;
33  import java.util.stream.Stream;
34  
35  @PhaseIndicator(phase = ProcessingPhase.DEPENDENCY_ANALYSIS)
36  public class FileCollisionAnalyser extends ModelHandlerBase {
37  
38      public FileCollisionAnalyser(Context context) {
39          super(context);
40      }
41  
42      @Override
43      protected Class<AppenderModel> getSupportedModelClass() {
44          return AppenderModel.class;
45      }
46  
47  
48      @Override
49      public void handle(ModelInterpretationContext mic, Model model) throws ModelHandlerException {
50          AppenderModel appenderModel = (AppenderModel) model;
51  
52          String originalClassName = appenderModel.getClassName();
53          String className = mic.getImport(originalClassName);
54  
55          String appenderName = appenderModel.getName();
56  
57          // A SiftingAppender instantiates its nested appender once per discriminator
58          // value at runtime, so the nested appender escapes the static collision maps
59          // handled below. If its file/fileNamePattern does not reference the
60          // discriminator key, every instance resolves to the same target and their
61          // output silently collides. Detect that at model-analysis time (see #1041).
62          Model siftModel = firstSubModelOfClass(appenderModel, SiftModel.class);
63          if (siftModel != null) {
64              checkSiftingAppenderForCollision(mic, appenderModel, siftModel, appenderName);
65              return;
66          }
67  
68          if (!fileAppenderOrRollingFileAppender(className)) {
69              return;
70          }
71  
72          String tagName0 = "file";
73          checkForCollisions(mic, MapKey.FILE_COLLISION_MAP_KEY, appenderModel, appenderName, tagName0);
74  
75          String tagName1 = "fileNamePattern";
76          checkForCollisions(mic, MapKey.RFA_FILENAME_COLLISION_MAP, appenderModel, appenderName, tagName1);
77      }
78  
79      private static boolean fileAppenderOrRollingFileAppender(String className) {
80          return FileAppender.class.getName().equals(className) || RollingFileAppender.class.getName().equals(className);
81      }
82  
83  
84      boolean tagPredicate(Model model, String tagName) {
85          return (model instanceof ImplicitModel) && tagName.equals(model.getTag());
86      }
87  
88      enum MapKey {
89          FILE_COLLISION_MAP_KEY, RFA_FILENAME_COLLISION_MAP
90      }
91  
92      private void checkForCollisions(ModelInterpretationContext mic, MapKey mapKey, AppenderModel appenderModel, String appenderName, final String tagName) {
93  
94  
95          Stream<Model> streamLevel1 = appenderModel.getSubModels().stream();
96          Stream<Model> streamLevel2 = appenderModel.getSubModels().stream().flatMap(child -> child.getSubModels().stream());
97  
98          List<Model> matchingModels = Stream.concat(streamLevel1, streamLevel2).filter(m -> tagPredicate(m, tagName)).collect(Collectors.toList());
99  
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 }