1   /**
2    * Logback: the reliable, generic, fast and flexible logging framework.
3    * Copyright (C) 1999-2015, QOS.ch. All rights reserved.
4    * <p>
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    * <p>
9    * or (per the licensee's choosing)
10   * <p>
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.jetty;
15  
16  import ch.qos.logback.access.common.joran.JoranConfigurator;
17  import ch.qos.logback.access.common.spi.AccessEvent;
18  import ch.qos.logback.access.common.spi.IAccessEvent;
19  import ch.qos.logback.core.Appender;
20  import ch.qos.logback.core.ContextBase;
21  import ch.qos.logback.core.CoreConstants;
22  import ch.qos.logback.core.boolex.EventEvaluator;
23  import ch.qos.logback.core.filter.Filter;
24  import ch.qos.logback.core.joran.spi.JoranException;
25  import ch.qos.logback.core.spi.AppenderAttachable;
26  import ch.qos.logback.core.spi.AppenderAttachableImpl;
27  import ch.qos.logback.core.spi.FilterAttachable;
28  import ch.qos.logback.core.spi.FilterAttachableImpl;
29  import ch.qos.logback.core.spi.FilterReply;
30  import ch.qos.logback.core.status.ErrorStatus;
31  import ch.qos.logback.core.status.InfoStatus;
32  import ch.qos.logback.core.util.FileUtil;
33  import ch.qos.logback.core.util.OptionHelper;
34  import ch.qos.logback.core.util.StatusPrinter;
35  import org.eclipse.jetty.server.Request;
36  import org.eclipse.jetty.server.RequestLog;
37  import org.eclipse.jetty.server.Response;
38  import org.eclipse.jetty.util.component.LifeCycle;
39  
40  import java.io.File;
41  import java.net.URL;
42  import java.util.EventListener;
43  import java.util.HashMap;
44  import java.util.Iterator;
45  import java.util.List;
46  
47  /**
48   * This class is logback's implementation of jetty's RequestLog interface.
49   * <p>
50   * It can be seen as logback classic's LoggerContext. Appenders can be attached
51   * directly to RequestLogImpl and RequestLogImpl uses the same StatusManager as
52   * LoggerContext does. It also provides containers for properties.
53   *
54   * </p>
55   * <h2>Supported Jetty Versions</h2>
56   * <p>
57   * This {@code RequestLogImpl} only supports Jetty 7.0.0 through Jetty 10.
58   * If you are using Jetty 11 with the new Jakarta Servlets (namespace {@code jakarta.servlet})
59   * then you will need a more modern version of {@code logback-access}.
60   * </p>
61   * <h2>Configuring for Jetty 9.4.x through to Jetty 10.0.x</h2>
62   * <p>
63   * Jetty 9.4.x and Jetty 10.x use a modern {@code org.eclipse.jetty.server.Server.setRequestLog(RequestLog)}
64   * interface that is based on a Server level RequestLog behavior.  This means all requests are logged,
65   * even bad requests, and context-less requests.
66   * </p>
67   * <p>
68   * The internals of the Jetty Request and Response objects track the state of the object at the time
69   * they are committed (the actual state during the application when an action on the network commits the
70   * request/response exchange).  This prevents behaviors from 3rd party libraries
71   * that change the state of the request / response before the RequestLog gets a chance
72   * to log the details.  This differs from Jetty 9.3.x and
73   * older in that those versions used a (now deprecated) {@code RequestLogHandler} and
74   * would never see bad requests, or context-less requests,
75   * and if a 3rd party library modifies the the response (for example by setting
76   * {@code response.setStatus(200)} after the response has been initiated on the network)
77   * this change in status would be logged, instead of the actual status that was sent.
78   * </p>
79   * <p>
80   * First, you must be using the proper {@code ${jetty.home}} and {@code ${jetty.base}}
81   * directory split.  Configure your {@code ${jetty.base}} with at least the `resources` module
82   * enabled (so that your configuration can be found).
83   * </p>
84   * <p>
85   * Next, create a {@code ${jetty.base}/etc/logback-access.xml} file with the following
86   * content.
87   * </p>
88   * <pre>
89   *   &lt;?xml version="1.0"?&gt;
90   *   &lt;!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"&gt;
91   *
92   *   &lt;Configure id="Server" class="org.eclipse.jetty.server.Server"&gt;
93   *     &lt;Set name="requestLog"&gt;
94   *       &lt;New id="LogbackAccess" class="ch.qos.logback.access.jetty.RequestLogImpl"&gt;
95   *         &lt;Set name="resource"&gt;logback-access.xml&lt;/Set&gt;
96   *       &lt;/New&gt;
97   *     &lt;/Set&gt;
98   *   &lt;/Configure&gt;</pre>
99   *
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  */
231 public 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     boolean quiet = false;
247 
248     public RequestLogImpl() {
249         putObject(CoreConstants.EVALUATOR_MAP, new HashMap<String, EventEvaluator<?>>());
250     }
251 
252     @Override
253     public void log(Request jettyRequest, Response jettyResponse) {
254         JettyServerAdapter adapter = makeJettyServerAdapter(jettyRequest, jettyResponse);
255         RequestWrapper requestWrapper = new RequestWrapper(jettyRequest);
256         ResponseWrapper responseWrapper = new ResponseWrapper(jettyResponse);
257 
258         IAccessEvent accessEvent = new AccessEvent(this, requestWrapper, responseWrapper, adapter);
259         if (getFilterChainDecision(accessEvent) == FilterReply.DENY) {
260             return;
261         }
262         aai.appendLoopOnAppenders(accessEvent);
263     }
264 
265     private JettyServerAdapter makeJettyServerAdapter(Request jettyRequest, Response jettyResponse) {
266        return new JettyModernServerAdapter(jettyRequest, jettyResponse);
267     }
268 
269     protected void addInfo(String msg) {
270         getStatusManager().add(new InfoStatus(msg, this));
271     }
272 
273     private void addError(String msg) {
274         getStatusManager().add(new ErrorStatus(msg, this));
275     }
276 
277     @Override
278     public void start() {
279         state = State.STARTING;
280         try {
281             configure();
282             if (!isQuiet()) {
283                 StatusPrinter.print(getStatusManager());
284             }
285             state = State.STARTED;
286         } catch (Throwable t) {
287             t.printStackTrace();
288             state = State.FAILED;
289         }
290     }
291 
292     protected void configure() {
293         URL configURL = getConfigurationFileURL();
294         if (configURL != null) {
295             runJoranOnFile(configURL);
296         } else {
297             addError("Could not find configuration file for logback-access");
298         }
299     }
300 
301     protected URL getConfigurationFileURL() {
302         if (fileName != null) {
303             addInfo("Will use configuration file [" + fileName + "]");
304             File file = new File(fileName);
305             if (!file.exists()) return null;
306             return FileUtil.fileToURL(file);
307         }
308         if (resource != null) {
309             addInfo("Will use configuration resource [" + resource + "]");
310             return this.getClass().getResource(resource);
311         }
312 
313         String defaultConfigFile = DEFAULT_CONFIG_FILE;
314         // Always attempt ${jetty.base} first
315         String jettyBaseProperty = OptionHelper.getSystemProperty("jetty.base");
316         if (!OptionHelper.isNullOrEmpty(jettyBaseProperty)) {
317             defaultConfigFile = jettyBaseProperty + File.separatorChar + DEFAULT_CONFIG_FILE;
318         }
319 
320         File file = new File(defaultConfigFile);
321         if (!file.exists()) {
322             // Then use ${jetty.home} (not supported in Jetty 10+)
323             String jettyHomeProperty = OptionHelper.getSystemProperty("jetty.home");
324             if (!OptionHelper.isEmpty(jettyHomeProperty)) {
325                 defaultConfigFile = jettyHomeProperty + File.separatorChar + DEFAULT_CONFIG_FILE;
326             } else {
327                 addInfo("Neither [jetty.base] nor [jetty.home] system properties are set.");
328             }
329         }
330 
331         file = new File(defaultConfigFile);
332         addInfo("Assuming default configuration file [" + defaultConfigFile + "]");
333         if (!file.exists()) return null;
334         return FileUtil.fileToURL(file);
335     }
336 
337     private void runJoranOnFile(URL configURL) {
338         try {
339             JoranConfigurator jc = new JoranConfigurator();
340             jc.setContext(this);
341             jc.doConfigure(configURL);
342             if (getName() == null) {
343                 setName("LogbackRequestLog");
344             }
345         } catch (JoranException e) {
346             // errors have been registered as status messages
347         }
348     }
349 
350     @Override
351     public void stop() {
352         state = State.STOPPING;
353         aai.detachAndStopAllAppenders();
354         state = State.STOPPED;
355     }
356 
357     @Override
358     public boolean isRunning() {
359         return state == State.STARTED;
360     }
361 
362     public void setFileName(String fileName) {
363         this.fileName = fileName;
364     }
365 
366     public void setResource(String resource) {
367         this.resource = resource;
368     }
369 
370     @Override
371     public boolean isStarted() {
372         return state == State.STARTED;
373     }
374 
375     @Override
376     public boolean isStarting() {
377         return state == State.STARTING;
378     }
379 
380     @Override
381     public boolean isStopping() {
382         return state == State.STOPPING;
383     }
384 
385     public boolean isStopped() {
386         return state == State.STOPPED;
387     }
388 
389     @Override
390     public boolean isFailed() {
391         return state == State.FAILED;
392     }
393 
394     @Override
395     public boolean addEventListener(EventListener listener) {
396         return false;
397     }
398 
399     @Override
400     public boolean removeEventListener(EventListener listener) {
401         return false;
402     }
403 
404 
405     public boolean isQuiet() {
406         return quiet;
407     }
408 
409     public void setQuiet(boolean quiet) {
410         this.quiet = quiet;
411     }
412 
413     @Override
414     public void addAppender(Appender<IAccessEvent> newAppender) {
415         aai.addAppender(newAppender);
416     }
417 
418     @Override
419     public Iterator<Appender<IAccessEvent>> iteratorForAppenders() {
420         return aai.iteratorForAppenders();
421     }
422 
423     @Override
424     public Appender<IAccessEvent> getAppender(String name) {
425         return aai.getAppender(name);
426     }
427 
428     @Override
429     public boolean isAttached(Appender<IAccessEvent> appender) {
430         return aai.isAttached(appender);
431     }
432 
433     @Override
434     public void detachAndStopAllAppenders() {
435         aai.detachAndStopAllAppenders();
436     }
437 
438     @Override
439     public boolean detachAppender(Appender<IAccessEvent> appender) {
440         return aai.detachAppender(appender);
441     }
442 
443     @Override
444     public boolean detachAppender(String name) {
445         return aai.detachAppender(name);
446     }
447 
448     @Override
449     public void addFilter(Filter<IAccessEvent> newFilter) {
450         fai.addFilter(newFilter);
451     }
452 
453     @Override
454     public void clearAllFilters() {
455         fai.clearAllFilters();
456     }
457 
458     @Override
459     public List<Filter<IAccessEvent>> getCopyOfAttachedFiltersList() {
460         return fai.getCopyOfAttachedFiltersList();
461     }
462 
463     @Override
464     public FilterReply getFilterChainDecision(IAccessEvent event) {
465         return fai.getFilterChainDecision(event);
466     }
467 
468     public void addLifeCycleListener(LifeCycle.Listener listener) {
469         // we'll implement this when asked
470     }
471 
472     public void removeLifeCycleListener(LifeCycle.Listener listener) {
473         // we'll implement this when asked
474     }
475 }