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.core.spi.ConfigurationEvent;
035import ch.qos.logback.core.spi.ConfigurationEventListener;
036import jakarta.servlet.ServletContext;
037import jakarta.servlet.ServletException;
038
039import org.apache.catalina.Lifecycle;
040import org.apache.catalina.LifecycleException;
041import org.apache.catalina.LifecycleListener;
042import org.apache.catalina.LifecycleState;
043import org.apache.catalina.connector.Request;
044import org.apache.catalina.connector.Response;
045import org.apache.catalina.valves.ValveBase;
046
047import ch.qos.logback.core.Appender;
048import ch.qos.logback.core.BasicStatusManager;
049import ch.qos.logback.core.Context;
050import ch.qos.logback.core.CoreConstants;
051import ch.qos.logback.core.LifeCycleManager;
052import ch.qos.logback.core.boolex.EventEvaluator;
053import ch.qos.logback.core.filter.Filter;
054import ch.qos.logback.core.joran.spi.JoranException;
055import ch.qos.logback.core.spi.AppenderAttachable;
056import ch.qos.logback.core.spi.AppenderAttachableImpl;
057import ch.qos.logback.core.spi.FilterAttachable;
058import ch.qos.logback.core.spi.FilterAttachableImpl;
059import ch.qos.logback.core.spi.FilterReply;
060import ch.qos.logback.core.spi.LifeCycle;
061import ch.qos.logback.core.spi.LogbackLock;
062import ch.qos.logback.core.spi.SequenceNumberGenerator;
063import ch.qos.logback.core.status.ErrorStatus;
064import ch.qos.logback.core.status.InfoStatus;
065import ch.qos.logback.core.status.OnConsoleStatusListener;
066import ch.qos.logback.core.status.Status;
067import ch.qos.logback.core.status.StatusManager;
068import ch.qos.logback.core.status.WarnStatus;
069import ch.qos.logback.core.util.ExecutorServiceUtil;
070import ch.qos.logback.core.util.Loader;
071import ch.qos.logback.core.util.OptionHelper;
072import ch.qos.logback.core.util.StatusListenerConfigHelper;
073
074//import org.apache.catalina.Lifecycle;
075
076/**
077 * This class is an implementation of tomcat's Valve interface, by extending
078 * ValveBase.
079 * 
080 * <p>
081 * For more information on using LogbackValve please refer to the online
082 * documentation on
083 * <a href="http://logback.qos.ch/access.html#tomcat">logback-access and
084 * tomcat</a>.
085 * 
086 * @author Ceki G&uuml;lc&uuml;
087 * @author S&eacute;bastien Pennec
088 */
089public class LogbackValve extends ValveBase
090        implements Lifecycle, Context, AppenderAttachable<IAccessEvent>, FilterAttachable<IAccessEvent> {
091
092    public final static String DEFAULT_FILENAME = "logback-access.xml";
093    public final static String DEFAULT_CONFIG_FILE = "conf" + File.separatorChar + DEFAULT_FILENAME;
094    final static String CATALINA_BASE_KEY = "catalina.base";
095    final static String CATALINA_HOME_KEY = "catalina.home";
096
097    private final LifeCycleManager lifeCycleManager = new LifeCycleManager();
098
099    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}