1   /**
2    * Logback: the reliable, generic, fast and flexible logging framework.
3    * Copyright (C) 1999-2015, 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 v1.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.access.tomcat;
15  
16  import java.io.File;
17  import java.io.IOException;
18  import java.net.MalformedURLException;
19  import java.net.URL;
20  import java.util.ArrayList;
21  import java.util.HashMap;
22  import java.util.Iterator;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.concurrent.ExecutorService;
26  import java.util.concurrent.ScheduledExecutorService;
27  import java.util.concurrent.ScheduledFuture;
28  import java.util.concurrent.locks.ReentrantLock;
29  
30  import ch.qos.logback.access.common.AccessConstants;
31  import ch.qos.logback.access.common.joran.JoranConfigurator;
32  import ch.qos.logback.access.common.spi.AccessEvent;
33  import ch.qos.logback.access.common.spi.IAccessEvent;
34  import ch.qos.logback.core.spi.ConfigurationEvent;
35  import ch.qos.logback.core.spi.ConfigurationEventListener;
36  import jakarta.servlet.ServletContext;
37  import jakarta.servlet.ServletException;
38  
39  import org.apache.catalina.Lifecycle;
40  import org.apache.catalina.LifecycleException;
41  import org.apache.catalina.LifecycleListener;
42  import org.apache.catalina.LifecycleState;
43  import org.apache.catalina.connector.Request;
44  import org.apache.catalina.connector.Response;
45  import org.apache.catalina.valves.ValveBase;
46  
47  import ch.qos.logback.core.Appender;
48  import ch.qos.logback.core.BasicStatusManager;
49  import ch.qos.logback.core.Context;
50  import ch.qos.logback.core.CoreConstants;
51  import ch.qos.logback.core.LifeCycleManager;
52  import ch.qos.logback.core.boolex.EventEvaluator;
53  import ch.qos.logback.core.filter.Filter;
54  import ch.qos.logback.core.joran.spi.JoranException;
55  import ch.qos.logback.core.spi.AppenderAttachable;
56  import ch.qos.logback.core.spi.AppenderAttachableImpl;
57  import ch.qos.logback.core.spi.FilterAttachable;
58  import ch.qos.logback.core.spi.FilterAttachableImpl;
59  import ch.qos.logback.core.spi.FilterReply;
60  import ch.qos.logback.core.spi.LifeCycle;
61  import ch.qos.logback.core.spi.LogbackLock;
62  import ch.qos.logback.core.spi.SequenceNumberGenerator;
63  import ch.qos.logback.core.status.ErrorStatus;
64  import ch.qos.logback.core.status.InfoStatus;
65  import ch.qos.logback.core.status.OnConsoleStatusListener;
66  import ch.qos.logback.core.status.Status;
67  import ch.qos.logback.core.status.StatusManager;
68  import ch.qos.logback.core.status.WarnStatus;
69  import ch.qos.logback.core.util.ExecutorServiceUtil;
70  import ch.qos.logback.core.util.Loader;
71  import ch.qos.logback.core.util.OptionHelper;
72  import ch.qos.logback.core.util.StatusListenerConfigHelper;
73  
74  //import org.apache.catalina.Lifecycle;
75  
76  /**
77   * This class is an implementation of tomcat's Valve interface, by extending
78   * ValveBase.
79   * 
80   * <p>
81   * For more information on using LogbackValve please refer to the online
82   * documentation on
83   * <a href="http://logback.qos.ch/access.html#tomcat">logback-access and
84   * tomcat</a>.
85   * 
86   * @author Ceki G&uuml;lc&uuml;
87   * @author S&eacute;bastien Pennec
88   */
89  public class LogbackValve extends ValveBase
90          implements Lifecycle, Context, AppenderAttachable<IAccessEvent>, FilterAttachable<IAccessEvent> {
91  
92      public final static String DEFAULT_FILENAME = "logback-access.xml";
93      public final static String DEFAULT_CONFIG_FILE = "conf" + File.separatorChar + DEFAULT_FILENAME;
94      final static String CATALINA_BASE_KEY = "catalina.base";
95      final static String CATALINA_HOME_KEY = "catalina.home";
96  
97      private final LifeCycleManager lifeCycleManager = new LifeCycleManager();
98  
99      private long birthTime = System.currentTimeMillis();
100 
101     ReentrantLock configurationLock = new ReentrantLock();
102 
103     final private List<ConfigurationEventListener> configurationEventListenerList = new ArrayList<>();
104 
105     // Attributes from ContextBase:
106     private String name;
107     StatusManager sm = new BasicStatusManager();
108     // TODO propertyMap should be observable so that we can be notified
109     // when it changes so that a new instance of propertyMap can be
110     // serialized. For the time being, we ignore this shortcoming.
111     Map<String, String> propertyMap = new HashMap<String, String>();
112     Map<String, Object> objectMap = new HashMap<String, Object>();
113     private FilterAttachableImpl<IAccessEvent> fai = new FilterAttachableImpl<IAccessEvent>();
114 
115     AppenderAttachableImpl<IAccessEvent> aai = new AppenderAttachableImpl<IAccessEvent>();
116     String filenameOption;
117     boolean quiet;
118     boolean started;
119     boolean alreadySetLogbackStatusManager = false;
120     private SequenceNumberGenerator sequenceNumberGenerator;
121 
122     private ScheduledExecutorService scheduledExecutorService;
123 
124     public LogbackValve() {
125         putObject(CoreConstants.EVALUATOR_MAP, new HashMap<String, EventEvaluator<?>>());
126     }
127 
128     public boolean isStarted() {
129         return started;
130     }
131 
132     @Override
133     public void startInternal() throws LifecycleException {
134         scheduledExecutorService = ExecutorServiceUtil.newScheduledExecutorService();
135 
136         String filename;
137 
138         if (filenameOption != null) {
139             filename = filenameOption;
140         } else {
141             addInfo("filename property not set. Assuming [" + DEFAULT_CONFIG_FILE + "]");
142             filename = DEFAULT_CONFIG_FILE;
143         }
144 
145         // String catalinaBase = OptionHelper.getSystemProperty(CATALINA_BASE_KEY);
146         // String catalinaHome = OptionHelper.getSystemProperty(CATALINA_BASE_KEY);
147 
148         File configFile = searchForConfigFileTomcatProperty(filename, CATALINA_BASE_KEY);
149         if (configFile == null) {
150             configFile = searchForConfigFileTomcatProperty(filename, CATALINA_HOME_KEY);
151         }
152 
153         URL resourceURL;
154         if (configFile != null)
155             resourceURL = fileToUrl(configFile);
156         else
157             resourceURL = searchAsResource(filename);
158 
159         if (resourceURL != null) {
160             configureAsResource(resourceURL);
161         } else {
162             addWarn("Failed to find valid logback-access configuration file.");
163         }
164 
165         if (!quiet) {
166             StatusListenerConfigHelper.addOnConsoleListenerInstance(this, new OnConsoleStatusListener());
167         }
168 
169         started = true;
170         setState(LifecycleState.STARTING);
171     }
172 
173     private URL fileToUrl(File configFile) {
174         try {
175             return configFile.toURI().toURL();
176         } catch (MalformedURLException e) {
177             throw new IllegalStateException("File to URL conversion failed", e);
178         }
179     }
180 
181     private URL searchAsResource(String filename) {
182         URL result = Loader.getResource(filename, getClass().getClassLoader());
183         if (result != null)
184             addInfo("Found [" + filename + "] as a resource.");
185         else
186             addInfo("Could NOT find [" + filename + "] as a resource.");
187         return result;
188     }
189 
190     private File searchForConfigFileTomcatProperty(String filename, String propertyKey) {
191         String propertyValue = OptionHelper.getSystemProperty(propertyKey);
192         String candidatePath = propertyValue + File.separatorChar + filename;
193         if (propertyValue == null) {
194             addInfo("System property \"" + propertyKey + "\" is not set. Skipping configuration file search with ${"
195                     + propertyKey + "} path prefix.");
196             return null;
197         }
198         File candidateFile = new File(candidatePath);
199         if (candidateFile.exists()) {
200             addInfo("Found configuration file [" + candidatePath + "] using property \"" + propertyKey + "\"");
201             return candidateFile;
202         } else {
203             addInfo("Could NOT find configuration file [" + candidatePath + "] using property \"" + propertyKey + "\"");
204             return null;
205         }
206     }
207 
208     public void addStatus(Status status) {
209         StatusManager sm = getStatusManager();
210         if (sm != null) {
211             sm.add(status);
212         }
213     }
214 
215     public void addInfo(String msg) {
216         addStatus(new InfoStatus(msg, this));
217     }
218 
219     public void addWarn(String msg) {
220         addStatus(new WarnStatus(msg, this));
221     }
222 
223     public void addError(String msg, Throwable t) {
224         addStatus(new ErrorStatus(msg, this, t));
225     }
226 
227     private void configureAsResource(URL resourceURL) {
228         try {
229             JoranConfigurator jc = new JoranConfigurator();
230             jc.setContext(this);
231             jc.doConfigure(resourceURL);
232             addInfo("Done configuring");
233         } catch (JoranException e) {
234             addError("Failed to configure LogbackValve", e);
235         }
236     }
237 
238     public String getFilename() {
239         return filenameOption;
240     }
241 
242     public void setFilename(String filename) {
243         this.filenameOption = filename;
244     }
245 
246     public boolean isQuiet() {
247         return quiet;
248     }
249 
250     public void setQuiet(boolean quiet) {
251         this.quiet = quiet;
252     }
253 
254     @Override
255     public void invoke(Request request, Response response) throws IOException, ServletException {
256         try {
257             if (!alreadySetLogbackStatusManager) {
258                 alreadySetLogbackStatusManager = true;
259                 org.apache.catalina.Context tomcatContext = request.getContext();
260                 if (tomcatContext != null) {
261                     ServletContext sc = tomcatContext.getServletContext();
262                     if (sc != null) {
263                         sc.setAttribute(AccessConstants.LOGBACK_STATUS_MANAGER_KEY, getStatusManager());
264                     }
265                 }
266             }
267 
268             getNext().invoke(request, response);
269 
270             TomcatServerAdapter adapter = new TomcatServerAdapter(request, response);
271             IAccessEvent accessEvent = new AccessEvent(this, request, response, adapter);
272 
273             addThreadName(accessEvent);
274 
275             if (getFilterChainDecision(accessEvent) == FilterReply.DENY) {
276                 return;
277             }
278 
279             // TODO better exception handling
280             aai.appendLoopOnAppenders(accessEvent);
281         } finally {
282             request.removeAttribute(AccessConstants.LOGBACK_STATUS_MANAGER_KEY);
283         }
284     }
285 
286     private void addThreadName(IAccessEvent accessEvent) {
287         try {
288             final String threadName = Thread.currentThread().getName();
289             if (threadName != null) {
290                 accessEvent.setThreadName(threadName);
291             }
292         } catch (Exception ignored) {
293         }
294     }
295 
296     @Override
297     protected void stopInternal() throws LifecycleException {
298         started = false;
299         setState(LifecycleState.STOPPING);
300         lifeCycleManager.reset();
301         if (scheduledExecutorService != null) {
302             ExecutorServiceUtil.shutdown(scheduledExecutorService);
303             scheduledExecutorService = null;
304         }
305     }
306 
307     @Override
308     public void addAppender(Appender<IAccessEvent> newAppender) {
309         aai.addAppender(newAppender);
310     }
311 
312     @Override
313     public Iterator<Appender<IAccessEvent>> iteratorForAppenders() {
314         return aai.iteratorForAppenders();
315     }
316 
317     @Override
318     public Appender<IAccessEvent> getAppender(String name) {
319         return aai.getAppender(name);
320     }
321 
322     @Override
323     public boolean isAttached(Appender<IAccessEvent> appender) {
324         return aai.isAttached(appender);
325     }
326 
327     @Override
328     public void detachAndStopAllAppenders() {
329         aai.detachAndStopAllAppenders();
330 
331     }
332 
333     @Override
334     public boolean detachAppender(Appender<IAccessEvent> appender) {
335         return aai.detachAppender(appender);
336     }
337 
338     @Override
339     public boolean detachAppender(String name) {
340         return aai.detachAppender(name);
341     }
342 
343     public String getInfo() {
344         return "Logback's implementation of ValveBase";
345     }
346 
347     // Methods from ContextBase:
348     @Override
349     public StatusManager getStatusManager() {
350         return sm;
351     }
352 
353     public Map<String, String> getPropertyMap() {
354         return propertyMap;
355     }
356 
357     @Override
358     public void addSubstitutionProperty(String key, String value) {
359         this.propertyMap.put(key, value);
360     }
361 
362     @Override
363     public void putProperty(String key, String val) {
364         this.propertyMap.put(key, val);
365     }
366 
367     @Override
368     public String getProperty(String key) {
369         return (String) this.propertyMap.get(key);
370     }
371 
372     @Override
373     public Map<String, String> getCopyOfPropertyMap() {
374         return new HashMap<String, String>(this.propertyMap);
375     }
376 
377     @Override
378     public Object getObject(String key) {
379         return objectMap.get(key);
380     }
381 
382     @Override
383     public void putObject(String key, Object value) {
384         objectMap.put(key, value);
385     }
386 
387     @Override
388     public void addFilter(Filter<IAccessEvent> newFilter) {
389         fai.addFilter(newFilter);
390     }
391 
392     @Override
393     public void clearAllFilters() {
394         fai.clearAllFilters();
395     }
396 
397     @Override
398     public List<Filter<IAccessEvent>> getCopyOfAttachedFiltersList() {
399         return fai.getCopyOfAttachedFiltersList();
400     }
401 
402     @Override
403     public FilterReply getFilterChainDecision(IAccessEvent event) {
404         return fai.getFilterChainDecision(event);
405     }
406 
407     @Override
408     public ExecutorService getExecutorService() {
409         return getScheduledExecutorService();
410     }
411 
412     @Override
413     public String getName() {
414         return name;
415     }
416 
417     @Override
418     public void setName(String name) {
419         if (this.name != null) {
420             throw new IllegalStateException("LogbackValve has been already given a name");
421         }
422         this.name = name;
423     }
424 
425     @Override
426     public long getBirthTime() {
427         return birthTime;
428     }
429 
430     @Override
431     public ReentrantLock getConfigurationLock() {
432         return configurationLock;
433     }
434 
435     @Override
436     public void register(LifeCycle component) {
437         lifeCycleManager.register(component);
438     }
439 
440     // ====== Methods from catalina Lifecycle =====
441 
442     @Override
443     public void addLifecycleListener(LifecycleListener arg0) {
444         // dummy NOP implementation
445     }
446 
447     @Override
448     public LifecycleListener[] findLifecycleListeners() {
449         return new LifecycleListener[0];
450     }
451 
452     @Override
453     public void removeLifecycleListener(LifecycleListener arg0) {
454         // dummy NOP implementation
455     }
456 
457     @Override
458     public String toString() {
459         StringBuilder sb = new StringBuilder(this.getClass().getName());
460         sb.append('[');
461         sb.append(getName());
462         sb.append(']');
463         return sb.toString();
464     }
465 
466     @Override
467     public ScheduledExecutorService getScheduledExecutorService() {
468         return scheduledExecutorService;
469     }
470 
471     @Override
472     public void addScheduledFuture(ScheduledFuture<?> scheduledFuture) {
473         throw new UnsupportedOperationException();
474     }
475 
476     public SequenceNumberGenerator getSequenceNumberGenerator() {
477         return sequenceNumberGenerator;
478     }
479 
480     public void setSequenceNumberGenerator(SequenceNumberGenerator sequenceNumberGenerator) {
481         this.sequenceNumberGenerator = sequenceNumberGenerator;
482     }
483 
484 
485     @Override
486     public void addConfigurationEventListener(ConfigurationEventListener listener) {
487         configurationEventListenerList.add(listener);
488     }
489 
490     @Override
491     public void fireConfigurationEvent(ConfigurationEvent configurationEvent) {
492         configurationEventListenerList.forEach( l -> l.listen(configurationEvent));
493     }
494 }