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