001/**
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2015, 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 v1.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.access.tomcat;
015
016import java.io.File;
017import java.io.IOException;
018import java.net.MalformedURLException;
019import java.net.URL;
020import java.util.ArrayList;
021import java.util.HashMap;
022import java.util.Iterator;
023import java.util.List;
024import java.util.Map;
025import java.util.concurrent.ExecutorService;
026import java.util.concurrent.ScheduledExecutorService;
027import java.util.concurrent.ScheduledFuture;
028import java.util.concurrent.locks.ReentrantLock;
029
030import ch.qos.logback.access.common.AccessConstants;
031import ch.qos.logback.access.common.joran.JoranConfigurator;
032import ch.qos.logback.access.common.spi.AccessEvent;
033import ch.qos.logback.access.common.spi.IAccessEvent;
034import ch.qos.logback.access.common.util.AccessCommonVersionUtil;
035import ch.qos.logback.core.spi.ConfigurationEvent;
036import ch.qos.logback.core.spi.ConfigurationEventListener;
037import ch.qos.logback.core.util.CoreVersionUtil;
038import ch.qos.logback.core.util.VersionUtil;
039import jakarta.servlet.ServletContext;
040import jakarta.servlet.ServletException;
041
042import org.apache.catalina.Lifecycle;
043import org.apache.catalina.LifecycleException;
044import org.apache.catalina.LifecycleListener;
045import org.apache.catalina.LifecycleState;
046import org.apache.catalina.connector.Request;
047import org.apache.catalina.connector.Response;
048import org.apache.catalina.valves.ValveBase;
049
050import ch.qos.logback.core.Appender;
051import ch.qos.logback.core.BasicStatusManager;
052import ch.qos.logback.core.Context;
053import ch.qos.logback.core.CoreConstants;
054import ch.qos.logback.core.LifeCycleManager;
055import ch.qos.logback.core.boolex.EventEvaluator;
056import ch.qos.logback.core.filter.Filter;
057import ch.qos.logback.core.joran.spi.JoranException;
058import ch.qos.logback.core.spi.AppenderAttachable;
059import ch.qos.logback.core.spi.AppenderAttachableImpl;
060import ch.qos.logback.core.spi.FilterAttachable;
061import ch.qos.logback.core.spi.FilterAttachableImpl;
062import ch.qos.logback.core.spi.FilterReply;
063import ch.qos.logback.core.spi.LifeCycle;
064import ch.qos.logback.core.spi.LogbackLock;
065import ch.qos.logback.core.spi.SequenceNumberGenerator;
066import ch.qos.logback.core.status.ErrorStatus;
067import ch.qos.logback.core.status.InfoStatus;
068import ch.qos.logback.core.status.OnConsoleStatusListener;
069import ch.qos.logback.core.status.Status;
070import ch.qos.logback.core.status.StatusManager;
071import ch.qos.logback.core.status.WarnStatus;
072import ch.qos.logback.core.util.ExecutorServiceUtil;
073import ch.qos.logback.core.util.Loader;
074import ch.qos.logback.core.util.OptionHelper;
075import ch.qos.logback.core.util.StatusListenerConfigHelper;
076
077//import org.apache.catalina.Lifecycle;
078
079/**
080 * This class is an implementation of tomcat's Valve interface, by extending
081 * ValveBase.
082 * 
083 * <p>
084 * For more information on using LogbackValve please refer to the online
085 * documentation on
086 * <a href="http://logback.qos.ch/access.html#tomcat">logback-access and
087 * tomcat</a>.
088 * 
089 * @author Ceki G&uuml;lc&uuml;
090 * @author S&eacute;bastien Pennec
091 */
092public class LogbackValve extends ValveBase
093        implements Lifecycle, Context, AppenderAttachable<IAccessEvent>, FilterAttachable<IAccessEvent> {
094
095    public final static String DEFAULT_FILENAME = "logback-access.xml";
096    public final static String DEFAULT_CONFIG_FILE = "conf" + File.separatorChar + DEFAULT_FILENAME;
097    final static String CATALINA_BASE_KEY = "catalina.base";
098    final static String CATALINA_HOME_KEY = "catalina.home";
099
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}