001/**
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2015, QOS.ch. All rights reserved.
004 * <p>
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 * <p>
009 * or (per the licensee's choosing)
010 * <p>
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.jetty;
015
016import java.io.File;
017import java.net.URL;
018import java.util.HashMap;
019import java.util.Iterator;
020import java.util.List;
021
022import ch.qos.logback.access.joran.JoranConfigurator;
023import ch.qos.logback.access.spi.AccessEvent;
024import ch.qos.logback.access.spi.IAccessEvent;
025import ch.qos.logback.core.Appender;
026import ch.qos.logback.core.ContextBase;
027import ch.qos.logback.core.CoreConstants;
028import ch.qos.logback.core.boolex.EventEvaluator;
029import ch.qos.logback.core.filter.Filter;
030import ch.qos.logback.core.joran.spi.JoranException;
031import ch.qos.logback.core.spi.AppenderAttachable;
032import ch.qos.logback.core.spi.AppenderAttachableImpl;
033import ch.qos.logback.core.spi.FilterAttachable;
034import ch.qos.logback.core.spi.FilterAttachableImpl;
035import ch.qos.logback.core.spi.FilterReply;
036import ch.qos.logback.core.status.ErrorStatus;
037import ch.qos.logback.core.status.InfoStatus;
038import ch.qos.logback.core.util.EnvUtil;
039import ch.qos.logback.core.util.FileUtil;
040import ch.qos.logback.core.util.OptionHelper;
041import ch.qos.logback.core.util.StatusPrinter;
042import org.eclipse.jetty.server.Request;
043import org.eclipse.jetty.server.RequestLog;
044import org.eclipse.jetty.server.Response;
045import org.eclipse.jetty.util.component.LifeCycle;
046
047/**
048 * This class is logback's implementation of jetty's RequestLog interface.
049 * <p>
050 * It can be seen as logback classic's LoggerContext. Appenders can be attached
051 * directly to RequestLogImpl and RequestLogImpl uses the same StatusManager as
052 * LoggerContext does. It also provides containers for properties.
053 *
054 * </p>
055 * <h2>Supported Jetty Versions</h2>
056 * <p>
057 * This {@code RequestLogImpl} only supports Jetty 7.0.0 through Jetty 10.
058 * If you are using Jetty 11 with the new Jakarta Servlets (namespace {@code jakarta.servlet})
059 * then you will need a more modern version of {@code logback-access}.
060 * </p>
061 * <h2>Configuring for Jetty 9.4.x through to Jetty 10.0.x</h2>
062 * <p>
063 * Jetty 9.4.x and Jetty 10.x use a modern {@code org.eclipse.jetty.server.Server.setRequestLog(RequestLog)}
064 * interface that is based on a Server level RequestLog behavior.  This means all requests are logged,
065 * even bad requests, and context-less requests.
066 * </p>
067 * <p>
068 * The internals of the Jetty Request and Response objects track the state of the object at the time
069 * they are committed (the actual state during the application when an action on the network commits the
070 * request/response exchange).  This prevents behaviors from 3rd party libraries
071 * that change the state of the request / response before the RequestLog gets a chance
072 * to log the details.  This differs from Jetty 9.3.x and
073 * older in that those versions used a (now deprecated) {@code RequestLogHandler} and
074 * would never see bad requests, or context-less requests,
075 * and if a 3rd party library modifies the the response (for example by setting
076 * {@code response.setStatus(200)} after the response has been initiated on the network)
077 * this change in status would be logged, instead of the actual status that was sent.
078 * </p>
079 * <p>
080 * First, you must be using the proper {@code ${jetty.home}} and {@code ${jetty.base}}
081 * directory split.  Configure your {@code ${jetty.base}} with at least the `resources` module
082 * enabled (so that your configuration can be found).
083 * </p>
084 * <p>
085 * Next, create a {@code ${jetty.base}/etc/logback-access.xml} file with the following
086 * content.
087 * </p>
088 * <pre>
089 *   &lt;?xml version="1.0"?&gt;
090 *   &lt;!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"&gt;
091 *
092 *   &lt;Configure id="Server" class="org.eclipse.jetty.server.Server"&gt;
093 *     &lt;Set name="requestLog"&gt;
094 *       &lt;New id="LogbackAccess" class="ch.qos.logback.access.jetty.RequestLogImpl"&gt;
095 *         &lt;Set name="resource"&gt;logback-access.xml&lt;/Set&gt;
096 *       &lt;/New&gt;
097 *     &lt;/Set&gt;
098 *   &lt;/Configure&gt;</pre>
099 *
100 * <p>
101 * Now you'll need a {@code ${jetty.base}/resources/logback-access.xml} configuration file.
102 * </p>
103 *
104 * <p>
105 * By default, {@code RequestLogImpl} looks for a logback configuration file called
106 * {@code etc/logback-access.xml}, in the {@code ${jetty.base}} directory, then
107 * the older {@code ${jetty.home}} directory.
108 * </p>
109 * <p>
110 * The {@code logback-access.xml} file is slightly
111 * different than the usual logback classic configuration file. Most of it is
112 * the same: {@link Appender Appenders} and {@link ch.qos.logback.core.Layout layouts}
113 * are declared the exact same way. However,
114 * loggers elements are not allowed.
115 * </p>
116 *
117 * <p> It is possible to place the logback configuration file anywhere, as long as it's path is specified.
118 * Here is another example, with an arbitrary path to the logback-access.xml file.
119 * <p/>
120 *
121 * <pre>
122 *   &lt;?xml version="1.0"?&gt;
123 *   &lt;!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"&gt;
124 *
125 *   &lt;Configure id="Server" class="org.eclipse.jetty.server.Server"&gt;
126 *     &lt;Set name="requestLog"&gt;
127 *       &lt;New id="LogbackAccess" class="ch.qos.logback.access.jetty.RequestLogImpl"&gt;
128 *         &lt;Set name="fileName"&gt;/arbitrary/path/to/logback-access.xml&lt;/Set&gt;
129 *       &lt;/New&gt;
130 *     &lt;/Set&gt;
131 *   &lt;/Configure&gt;
132 * </pre>
133 * <h2>Configuring for Jetty 7.x thru to Jetty 9.3.x</h2>
134 * <p>
135 * To configure these older Jetty instances to use {@code RequestLogImpl},
136 * the use of the {@code RequestLogHandler} is the technique available to you.
137 * Modify your {@code etc/jetty-requestlog.xml}
138 * </p>
139 *
140 * <pre>
141 *   &lt;?xml version="1.0"?&gt;
142 *   &lt;!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd"&gt;
143 *
144 *   &lt;Configure id="Server" class="org.eclipse.jetty.server.Server"&gt;
145 *     &lt;Ref id="Handlers"&gt;
146 *       &lt;Call name="addHandler"&gt;
147 *         &lt;Arg&gt;
148 *           &lt;New id="RequestLog" class="org.eclipse.jetty.server.handler.RequestLogHandler"&gt;
149 *             &lt;Set name="requestLog"&gt;
150 *               &lt;New id="RequestLogImpl" class="ch.qos.logback.access.jetty.RequestLogImpl"/&gt;
151 *             &lt;/Set&gt;
152 *           &lt;/New&gt;
153 *         &lt;/Arg&gt;
154 *       &lt;/Call&gt;
155 *     &lt;/Ref&gt;
156 *   &lt;/Configure&gt;
157 * </pre>
158 *
159 * <p>By default, RequestLogImpl looks for a logback configuration file called
160 * logback-access.xml, in the same folder where jetty.xml is located, that is
161 * <em>etc/logback-access.xml</em>. The logback-access.xml file is slightly
162 * different from the usual logback classic configuration file. Most of it is
163 * the same: Appenders and Layouts are declared the exact same way. However,
164 * loggers elements are not allowed.
165 * </p>
166 *
167 * <p>
168 * It is possible to put the logback configuration file anywhere, as long as
169 * it's path is specified. Here is another example, with a path to the
170 * logback-access.xml file.
171 * <p/>
172 *
173 * <pre>
174 *   &lt;?xml version="1.0"?&gt;
175 *   &lt;!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd"&gt;
176 *
177 *   &lt;Configure id="Server" class="org.eclipse.jetty.server.Server"&gt;
178 *     &lt;Ref id="Handlers"&gt;
179 *       &lt;Call name="addHandler"&gt;
180 *         &lt;Arg&gt;
181 *           &lt;New id="RequestLog" class="org.eclipse.jetty.server.handler.RequestLogHandler"&gt;
182 *             &lt;Set name="requestLog"&gt;
183 *               &lt;New id="RequestLogImpl" class="ch.qos.logback.access.jetty.RequestLogImpl"&gt;
184 *                 &lt;Set name="fileName"&gt;path/to/logback-access.xml&lt;/Set&gt;
185 *               &lt;/New&gt;
186 *             &lt;/Set&gt;
187 *           &lt;/New&gt;
188 *         &lt;/Arg&gt;
189 *       &lt;/Call&gt;
190 *     &lt;/Ref&gt;
191 *   &lt;/Configure&gt;
192 * </pre>
193 * <p>
194 * Next is a sample logback-access.xml file printing access events on the console.
195 * <p/>
196 *
197 * <pre>
198 *    &lt;configuration&gt;
199 *      &lt;appender name=&quot;STDOUT&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&gt;
200 *        &lt;layout class=&quot;ch.qos.logback.access.PatternLayout&quot;&gt;
201 *          &lt;param name=&quot;Pattern&quot; value=&quot;%date %server %remoteIP %clientHost %user %requestURL&quot; /&gt;
202 *        &lt;/layout&gt;
203 *      &lt;/appender&gt;
204 *
205 *      &lt;appender-ref ref=&quot;STDOUT&quot; /&gt;
206 *    &lt;/configuration&gt;
207 * </pre>
208 * <p>
209 * Here is another configuration file, using SMTPAppender:
210 * <p/>
211 *
212 * <pre>
213 *    &lt;configuration&gt;
214 *      &lt;appender name=&quot;SMTP&quot; class=&quot;ch.qos.logback.access.net.SMTPAppender&quot;&gt;
215 *        &lt;layout class=&quot;ch.qos.logback.access.PatternLayout&quot;&gt;
216 *          &lt;param name=&quot;pattern&quot; value=&quot;%remoteIP [%date] %requestURL %statusCode %bytesSent&quot; /&gt;
217 *        &lt;/layout&gt;
218 *        &lt;param name=&quot;From&quot; value=&quot;sender@domaine.org&quot; /&gt;
219 *        &lt;param name=&quot;SMTPHost&quot; value=&quot;mail.domain.org&quot; /&gt;
220 *         &lt;param name=&quot;Subject&quot; value=&quot;Last Event: %statusCode %requestURL&quot; /&gt;
221 *         &lt;param name=&quot;To&quot; value=&quot;server_admin@domain.org&quot; /&gt;
222 *      &lt;/appender&gt;
223 *      &lt;appender-ref ref=&quot;SMTP&quot; /&gt;
224 *    &lt;/configuration&gt;
225 * </pre>
226 *
227 * @author Ceki G&uuml;lc&uuml;
228 * @author S&eacute;bastien Pennec
229 * @author Joakim Erdfelt
230 */
231public class RequestLogImpl extends ContextBase implements org.eclipse.jetty.util.component.LifeCycle, RequestLog, AppenderAttachable<IAccessEvent>, FilterAttachable<IAccessEvent> {
232
233    public final static String DEFAULT_CONFIG_FILE = "etc" + File.separatorChar + "logback-access.xml";
234
235    enum State {
236        FAILED, STOPPED, STARTING, STARTED, STOPPING
237    }
238
239    State state = State.STOPPED;
240
241    AppenderAttachableImpl<IAccessEvent> aai = new AppenderAttachableImpl<IAccessEvent>();
242    FilterAttachableImpl<IAccessEvent> fai = new FilterAttachableImpl<IAccessEvent>();
243    String fileName;
244    String resource;
245
246    // Jetty 9.4.x and newer is considered modern.
247    boolean modernJettyRequestLog;
248    boolean quiet = false;
249
250    public RequestLogImpl() {
251        putObject(CoreConstants.EVALUATOR_MAP, new HashMap<String, EventEvaluator<?>>());
252
253        // plumb the depths of Jetty and the environment ...
254        if (EnvUtil.isClassAvailable(this.getClass(), "jakarta.servlet.http.HttpServlet")) {
255            throw new RuntimeException("The new jakarta.servlet classes are not supported by this " + "version of logback-access (check for a newer version of logback-access that " + "does support it)");
256        }
257
258        // look for modern approach to RequestLog
259        modernJettyRequestLog = EnvUtil.isClassAvailable(this.getClass(), "org.eclipse.jetty.server.CustomRequestLog");
260    }
261
262    @Override
263    public void log(Request jettyRequest, Response jettyResponse) {
264        JettyServerAdapter adapter = makeJettyServerAdapter(jettyRequest, jettyResponse);
265        IAccessEvent accessEvent = new AccessEvent(this, jettyRequest, jettyResponse, adapter);
266        if (getFilterChainDecision(accessEvent) == FilterReply.DENY) {
267            return;
268        }
269        aai.appendLoopOnAppenders(accessEvent);
270    }
271
272    private JettyServerAdapter makeJettyServerAdapter(Request jettyRequest, Response jettyResponse) {
273        if (modernJettyRequestLog) {
274            return new JettyModernServerAdapter(jettyRequest, jettyResponse);
275        } else {
276            return new JettyServerAdapter(jettyRequest, jettyResponse);
277        }
278    }
279
280    protected void addInfo(String msg) {
281        getStatusManager().add(new InfoStatus(msg, this));
282    }
283
284    private void addError(String msg) {
285        getStatusManager().add(new ErrorStatus(msg, this));
286    }
287
288    @Override
289    public void start() {
290        state = State.STARTING;
291        try {
292            configure();
293            if (!isQuiet()) {
294                StatusPrinter.print(getStatusManager());
295            }
296            state = State.STARTED;
297        } catch (Throwable t) {
298            t.printStackTrace();
299            state = State.FAILED;
300        }
301    }
302
303    protected void configure() {
304        URL configURL = getConfigurationFileURL();
305        if (configURL != null) {
306            runJoranOnFile(configURL);
307        } else {
308            addError("Could not find configuration file for logback-access");
309        }
310    }
311
312    protected URL getConfigurationFileURL() {
313        if (fileName != null) {
314            addInfo("Will use configuration file [" + fileName + "]");
315            File file = new File(fileName);
316            if (!file.exists()) return null;
317            return FileUtil.fileToURL(file);
318        }
319        if (resource != null) {
320            addInfo("Will use configuration resource [" + resource + "]");
321            return this.getClass().getResource(resource);
322        }
323
324        String defaultConfigFile = DEFAULT_CONFIG_FILE;
325        // Always attempt ${jetty.base} first
326        String jettyBaseProperty = OptionHelper.getSystemProperty("jetty.base");
327        if (!OptionHelper.isNullOrEmpty(jettyBaseProperty)) {
328            defaultConfigFile = jettyBaseProperty + File.separatorChar + DEFAULT_CONFIG_FILE;
329        }
330
331        File file = new File(defaultConfigFile);
332        if (!file.exists()) {
333            // Then use ${jetty.home} (not supported in Jetty 10+)
334            String jettyHomeProperty = OptionHelper.getSystemProperty("jetty.home");
335            if (!OptionHelper.isEmpty(jettyHomeProperty)) {
336                defaultConfigFile = jettyHomeProperty + File.separatorChar + DEFAULT_CONFIG_FILE;
337            } else {
338                addInfo("Neither [jetty.base] nor [jetty.home] system properties are set.");
339            }
340        }
341
342        file = new File(defaultConfigFile);
343        addInfo("Assuming default configuration file [" + defaultConfigFile + "]");
344        if (!file.exists()) return null;
345        return FileUtil.fileToURL(file);
346    }
347
348    private void runJoranOnFile(URL configURL) {
349        try {
350            JoranConfigurator jc = new JoranConfigurator();
351            jc.setContext(this);
352            jc.doConfigure(configURL);
353            if (getName() == null) {
354                setName("LogbackRequestLog");
355            }
356        } catch (JoranException e) {
357            // errors have been registered as status messages
358        }
359    }
360
361    @Override
362    public void stop() {
363        state = State.STOPPING;
364        aai.detachAndStopAllAppenders();
365        state = State.STOPPED;
366    }
367
368    @Override
369    public boolean isRunning() {
370        return state == State.STARTED;
371    }
372
373    public void setFileName(String fileName) {
374        this.fileName = fileName;
375    }
376
377    public void setResource(String resource) {
378        this.resource = resource;
379    }
380
381    @Override
382    public boolean isStarted() {
383        return state == State.STARTED;
384    }
385
386    @Override
387    public boolean isStarting() {
388        return state == State.STARTING;
389    }
390
391    @Override
392    public boolean isStopping() {
393        return state == State.STOPPING;
394    }
395
396    public boolean isStopped() {
397        return state == State.STOPPED;
398    }
399
400    @Override
401    public boolean isFailed() {
402        return state == State.FAILED;
403    }
404
405
406    public boolean isQuiet() {
407        return quiet;
408    }
409
410    public void setQuiet(boolean quiet) {
411        this.quiet = quiet;
412    }
413
414    @Override
415    public void addAppender(Appender<IAccessEvent> newAppender) {
416        aai.addAppender(newAppender);
417    }
418
419    @Override
420    public Iterator<Appender<IAccessEvent>> iteratorForAppenders() {
421        return aai.iteratorForAppenders();
422    }
423
424    @Override
425    public Appender<IAccessEvent> getAppender(String name) {
426        return aai.getAppender(name);
427    }
428
429    @Override
430    public boolean isAttached(Appender<IAccessEvent> appender) {
431        return aai.isAttached(appender);
432    }
433
434    @Override
435    public void detachAndStopAllAppenders() {
436        aai.detachAndStopAllAppenders();
437    }
438
439    @Override
440    public boolean detachAppender(Appender<IAccessEvent> appender) {
441        return aai.detachAppender(appender);
442    }
443
444    @Override
445    public boolean detachAppender(String name) {
446        return aai.detachAppender(name);
447    }
448
449    @Override
450    public void addFilter(Filter<IAccessEvent> newFilter) {
451        fai.addFilter(newFilter);
452    }
453
454    @Override
455    public void clearAllFilters() {
456        fai.clearAllFilters();
457    }
458
459    @Override
460    public List<Filter<IAccessEvent>> getCopyOfAttachedFiltersList() {
461        return fai.getCopyOfAttachedFiltersList();
462    }
463
464    @Override
465    public FilterReply getFilterChainDecision(IAccessEvent event) {
466        return fai.getFilterChainDecision(event);
467    }
468
469
470    @Override
471    public void addLifeCycleListener(LifeCycle.Listener listener) {
472        // we'll implement this when asked
473    }
474
475    @Override
476    public void removeLifeCycleListener(LifeCycle.Listener listener) {
477        // we'll implement this when asked
478    }
479}