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.AccessConstants;
17  import ch.qos.logback.access.common.joran.JoranConfigurator;
18  import ch.qos.logback.access.common.spi.AccessEvent;
19  import ch.qos.logback.access.common.spi.IAccessEvent;
20  import ch.qos.logback.access.common.util.AccessCommonVersionUtil;
21  import ch.qos.logback.core.Appender;
22  import ch.qos.logback.core.ContextBase;
23  import ch.qos.logback.core.CoreConstants;
24  import ch.qos.logback.core.boolex.EventEvaluator;
25  import ch.qos.logback.core.filter.Filter;
26  import ch.qos.logback.core.joran.spi.JoranException;
27  import ch.qos.logback.core.spi.AppenderAttachable;
28  import ch.qos.logback.core.spi.AppenderAttachableImpl;
29  import ch.qos.logback.core.spi.FilterAttachable;
30  import ch.qos.logback.core.spi.FilterAttachableImpl;
31  import ch.qos.logback.core.spi.FilterReply;
32  import ch.qos.logback.core.status.ErrorStatus;
33  import ch.qos.logback.core.status.InfoStatus;
34  import ch.qos.logback.core.status.WarnStatus;
35  import ch.qos.logback.core.util.CoreVersionUtil;
36  import ch.qos.logback.core.util.FileUtil;
37  import ch.qos.logback.core.util.OptionHelper;
38  import ch.qos.logback.core.util.StatusPrinter;
39  import ch.qos.logback.core.util.VersionUtil;
40  import org.eclipse.jetty.server.Request;
41  import org.eclipse.jetty.server.RequestLog;
42  import org.eclipse.jetty.server.Response;
43  import org.eclipse.jetty.util.component.LifeCycle;
44  
45  import java.io.File;
46  import java.net.URL;
47  import java.util.EventListener;
48  import java.util.HashMap;
49  import java.util.Iterator;
50  import java.util.List;
51  
52  import static ch.qos.logback.core.CoreConstants.DEFAULT_CONTEXT_NAME;
53  
54  /**
55   * This class is logback's implementation of jetty's RequestLog interface.
56   * <p>
57   * It can be seen as logback classic's LoggerContext. Appenders can be attached
58   * directly to RequestLogImpl and RequestLogImpl uses the same StatusManager as
59   * LoggerContext does. It also provides containers for properties.
60   *
61   * </p>
62   * <h2>Supported Jetty Versions</h2>
63   * <p>
64   * This {@code RequestLogImpl} only supports Jetty 7.0.0 through Jetty 10.
65   * If you are using Jetty 11 with the new Jakarta Servlets (namespace {@code jakarta.servlet})
66   * then you will need a more modern version of {@code logback-access}.
67   * </p>
68   * <h2>Configuring for Jetty 9.4.x through to Jetty 10.0.x</h2>
69   * <p>
70   * Jetty 9.4.x and Jetty 10.x use a modern {@code org.eclipse.jetty.server.Server.setRequestLog(RequestLog)}
71   * interface that is based on a Server level RequestLog behavior.  This means all requests are logged,
72   * even bad requests, and context-less requests.
73   * </p>
74   * <p>
75   * The internals of the Jetty Request and Response objects track the state of the object at the time
76   * they are committed (the actual state during the application when an action on the network commits the
77   * request/response exchange).  This prevents behaviors from 3rd party libraries
78   * that change the state of the request / response before the RequestLog gets a chance
79   * to log the details.  This differs from Jetty 9.3.x and
80   * older in that those versions used a (now deprecated) {@code RequestLogHandler} and
81   * would never see bad requests, or context-less requests,
82   * and if a 3rd party library modifies the the response (for example by setting
83   * {@code response.setStatus(200)} after the response has been initiated on the network)
84   * this change in status would be logged, instead of the actual status that was sent.
85   * </p>
86   * <p>
87   * First, you must be using the proper {@code ${jetty.home}} and {@code ${jetty.base}}
88   * directory split.  Configure your {@code ${jetty.base}} with at least the `resources` module
89   * enabled (so that your configuration can be found).
90   * </p>
91   * <p>
92   * Next, create a {@code ${jetty.base}/etc/logback-access.xml} file with the following
93   * content.
94   * </p>
95   * <pre>
96   *   &lt;?xml version="1.0"?&gt;
97   *   &lt;!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"&gt;
98   *
99   *   &lt;Configure id="Server" class="org.eclipse.jetty.server.Server"&gt;
100  *     &lt;Set name="requestLog"&gt;
101  *       &lt;New id="LogbackAccess" class="ch.qos.logback.access.jetty.RequestLogImpl"&gt;
102  *         &lt;Set name="resource"&gt;logback-access.xml&lt;/Set&gt;
103  *       &lt;/New&gt;
104  *     &lt;/Set&gt;
105  *   &lt;/Configure&gt;</pre>
106  *
107  * <p>
108  * Now you'll need a {@code ${jetty.base}/resources/logback-access.xml} configuration file.
109  * </p>
110  *
111  * <p>
112  * By default, {@code RequestLogImpl} looks for a logback configuration file called
113  * {@code etc/logback-access.xml}, in the {@code ${jetty.base}} directory, then
114  * the older {@code ${jetty.home}} directory.
115  * </p>
116  * <p>
117  * The {@code logback-access.xml} file is slightly
118  * different than the usual logback classic configuration file. Most of it is
119  * the same: {@link Appender Appenders} and {@link ch.qos.logback.core.Layout layouts}
120  * are declared the exact same way. However,
121  * loggers elements are not allowed.
122  * </p>
123  *
124  * <p> It is possible to place the logback configuration file anywhere, as long as it's path is specified.
125  * Here is another example, with an arbitrary path to the logback-access.xml file.
126  * <p/>
127  *
128  * <pre>
129  *   &lt;?xml version="1.0"?&gt;
130  *   &lt;!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"&gt;
131  *
132  *   &lt;Configure id="Server" class="org.eclipse.jetty.server.Server"&gt;
133  *     &lt;Set name="requestLog"&gt;
134  *       &lt;New id="LogbackAccess" class="ch.qos.logback.access.jetty.RequestLogImpl"&gt;
135  *         &lt;Set name="fileName"&gt;/arbitrary/path/to/logback-access.xml&lt;/Set&gt;
136  *       &lt;/New&gt;
137  *     &lt;/Set&gt;
138  *   &lt;/Configure&gt;
139  * </pre>
140  * <h2>Configuring for Jetty 7.x thru to Jetty 9.3.x</h2>
141  * <p>
142  * To configure these older Jetty instances to use {@code RequestLogImpl},
143  * the use of the {@code RequestLogHandler} is the technique available to you.
144  * Modify your {@code etc/jetty-requestlog.xml}
145  * </p>
146  *
147  * <pre>
148  *   &lt;?xml version="1.0"?&gt;
149  *   &lt;!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd"&gt;
150  *
151  *   &lt;Configure id="Server" class="org.eclipse.jetty.server.Server"&gt;
152  *     &lt;Ref id="Handlers"&gt;
153  *       &lt;Call name="addHandler"&gt;
154  *         &lt;Arg&gt;
155  *           &lt;New id="RequestLog" class="org.eclipse.jetty.server.handler.RequestLogHandler"&gt;
156  *             &lt;Set name="requestLog"&gt;
157  *               &lt;New id="RequestLogImpl" class="ch.qos.logback.access.jetty.RequestLogImpl"/&gt;
158  *             &lt;/Set&gt;
159  *           &lt;/New&gt;
160  *         &lt;/Arg&gt;
161  *       &lt;/Call&gt;
162  *     &lt;/Ref&gt;
163  *   &lt;/Configure&gt;
164  * </pre>
165  *
166  * <p>By default, RequestLogImpl looks for a logback configuration file called
167  * logback-access.xml, in the same folder where jetty.xml is located, that is
168  * <em>etc/logback-access.xml</em>. The logback-access.xml file is slightly
169  * different from the usual logback classic configuration file. Most of it is
170  * the same: Appenders and Layouts are declared the exact same way. However,
171  * loggers elements are not allowed.
172  * </p>
173  *
174  * <p>
175  * It is possible to put the logback configuration file anywhere, as long as
176  * it's path is specified. Here is another example, with a path to the
177  * logback-access.xml file.
178  * <p/>
179  *
180  * <pre>
181  *   &lt;?xml version="1.0"?&gt;
182  *   &lt;!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd"&gt;
183  *
184  *   &lt;Configure id="Server" class="org.eclipse.jetty.server.Server"&gt;
185  *     &lt;Ref id="Handlers"&gt;
186  *       &lt;Call name="addHandler"&gt;
187  *         &lt;Arg&gt;
188  *           &lt;New id="RequestLog" class="org.eclipse.jetty.server.handler.RequestLogHandler"&gt;
189  *             &lt;Set name="requestLog"&gt;
190  *               &lt;New id="RequestLogImpl" class="ch.qos.logback.access.jetty.RequestLogImpl"&gt;
191  *                 &lt;Set name="fileName"&gt;path/to/logback-access.xml&lt;/Set&gt;
192  *               &lt;/New&gt;
193  *             &lt;/Set&gt;
194  *           &lt;/New&gt;
195  *         &lt;/Arg&gt;
196  *       &lt;/Call&gt;
197  *     &lt;/Ref&gt;
198  *   &lt;/Configure&gt;
199  * </pre>
200  * <p>
201  * Next is a sample logback-access.xml file printing access events on the console.
202  * <p/>
203  *
204  * <pre>
205  *    &lt;configuration&gt;
206  *      &lt;appender name=&quot;STDOUT&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&gt;
207  *        &lt;layout class=&quot;ch.qos.logback.access.PatternLayout&quot;&gt;
208  *          &lt;param name=&quot;Pattern&quot; value=&quot;%date %server %remoteIP %clientHost %user %requestURL&quot; /&gt;
209  *        &lt;/layout&gt;
210  *      &lt;/appender&gt;
211  *
212  *      &lt;appender-ref ref=&quot;STDOUT&quot; /&gt;
213  *    &lt;/configuration&gt;
214  * </pre>
215  * <p>
216  * Here is another configuration file, using SMTPAppender:
217  * <p/>
218  *
219  * <pre>
220  *    &lt;configuration&gt;
221  *      &lt;appender name=&quot;SMTP&quot; class=&quot;ch.qos.logback.access.net.SMTPAppender&quot;&gt;
222  *        &lt;layout class=&quot;ch.qos.logback.access.PatternLayout&quot;&gt;
223  *          &lt;param name=&quot;pattern&quot; value=&quot;%remoteIP [%date] %requestURL %statusCode %bytesSent&quot; /&gt;
224  *        &lt;/layout&gt;
225  *        &lt;param name=&quot;From&quot; value=&quot;sender@domaine.org&quot; /&gt;
226  *        &lt;param name=&quot;SMTPHost&quot; value=&quot;mail.domain.org&quot; /&gt;
227  *         &lt;param name=&quot;Subject&quot; value=&quot;Last Event: %statusCode %requestURL&quot; /&gt;
228  *         &lt;param name=&quot;To&quot; value=&quot;server_admin@domain.org&quot; /&gt;
229  *      &lt;/appender&gt;
230  *      &lt;appender-ref ref=&quot;SMTP&quot; /&gt;
231  *    &lt;/configuration&gt;
232  * </pre>
233  *
234  * @author Ceki G&uuml;lc&uuml;
235  * @author S&eacute;bastien Pennec
236  * @author Joakim Erdfelt
237  */
238 public class RequestLogImpl extends ContextBase implements org.eclipse.jetty.util.component.LifeCycle, RequestLog, AppenderAttachable<IAccessEvent>, FilterAttachable<IAccessEvent> {
239 
240     public final static String DEFAULT_CONFIG_FILE = "etc" + File.separatorChar + "logback-access.xml";
241 
242     enum State {
243         FAILED, STOPPED, STARTING, STARTED, STOPPING
244     }
245 
246     State state = State.STOPPED;
247 
248     AppenderAttachableImpl<IAccessEvent> aai = new AppenderAttachableImpl<IAccessEvent>();
249     FilterAttachableImpl<IAccessEvent> fai = new FilterAttachableImpl<IAccessEvent>();
250     String fileName;
251     String resource;
252 
253     boolean quiet = false;
254 
255     public RequestLogImpl() {
256         setName(DEFAULT_CONTEXT_NAME);
257         putObject(CoreConstants.EVALUATOR_MAP, new HashMap<String, EventEvaluator<?>>());
258     }
259 
260     @Override
261     public void log(Request jettyRequest, Response jettyResponse) {
262         JettyServerAdapter adapter = makeJettyServerAdapter(jettyRequest, jettyResponse);
263         RequestWrapper requestWrapper = new RequestWrapper(jettyRequest);
264         ResponseWrapper responseWrapper = new ResponseWrapper(jettyResponse);
265 
266         IAccessEvent accessEvent = new AccessEvent(this, requestWrapper, responseWrapper, adapter);
267         if (getFilterChainDecision(accessEvent) == FilterReply.DENY) {
268             return;
269         }
270         aai.appendLoopOnAppenders(accessEvent);
271     }
272 
273     private JettyServerAdapter makeJettyServerAdapter(Request jettyRequest, Response jettyResponse) {
274        return new JettyModernServerAdapter(jettyRequest, jettyResponse);
275     }
276 
277     protected void addInfo(String msg) {
278         getStatusManager().add(new InfoStatus(msg, this));
279     }
280 
281     protected void addWarn(String msg) {
282         getStatusManager().add(new WarnStatus(msg, this));
283     }
284 
285     private void addError(String msg) {
286         getStatusManager().add(new ErrorStatus(msg, this));
287     }
288 
289     @Override
290     public void start() {
291         state = State.STARTING;
292 
293         versionCheck();
294         try {
295             configure();
296             if (!isQuiet()) {
297                 StatusPrinter.print(getStatusManager());
298             }
299             state = State.STARTED;
300         } catch (Throwable t) {
301             t.printStackTrace();
302             state = State.FAILED;
303         }
304     }
305 
306     static String LOGBACK_ACCESS_JETTY12_NAME = "logback-access-jetty12";
307     static String LOGBACK_ACCESS_COMMON_NAME = "logback-access-common";
308     static String LOGBACK_CORE_NAME = "logback-core";
309 
310     private void versionCheck() {
311         try {
312             String coreVersion = CoreVersionUtil.getCoreVersionBySelfDeclaredProperties();
313             String accessCommonVersion = AccessCommonVersionUtil.getAccessCommonVersionBySelfDeclaredProperties();
314             VersionUtil.checkForVersionEquality(this, this.getClass(), accessCommonVersion, LOGBACK_ACCESS_JETTY12_NAME, LOGBACK_ACCESS_COMMON_NAME);
315             VersionUtil.compareExpectedAndFoundVersion(this, coreVersion, AccessConstants.class, accessCommonVersion, LOGBACK_ACCESS_COMMON_NAME, LOGBACK_CORE_NAME);
316         } catch(NoClassDefFoundError e) {
317             addWarn("Missing ch.logback.core.util.VersionUtil class on classpath. The version of logback-core is probably earlier than 1.5.25.");
318         } catch(NoSuchMethodError e) {
319             addWarn(e.getMessage() + ". The version of logback-core is probably earlier than 1.5.26.");
320         }
321     }
322 
323 
324     protected void configure() {
325         URL configURL = getConfigurationFileURL();
326         if (configURL != null) {
327             runJoranOnFile(configURL);
328         } else {
329             addError("Could not find configuration file for logback-access");
330         }
331     }
332 
333     protected URL getConfigurationFileURL() {
334         if (fileName != null) {
335             addInfo("Will use configuration file [" + fileName + "]");
336             File file = new File(fileName);
337             if (!file.exists()) return null;
338             return FileUtil.fileToURL(file);
339         }
340         if (resource != null) {
341             addInfo("Will use configuration resource [" + resource + "]");
342             return this.getClass().getResource(resource);
343         }
344 
345         String defaultConfigFile = DEFAULT_CONFIG_FILE;
346         // Always attempt ${jetty.base} first
347         String jettyBaseProperty = OptionHelper.getSystemProperty("jetty.base");
348         if (!OptionHelper.isNullOrEmpty(jettyBaseProperty)) {
349             defaultConfigFile = jettyBaseProperty + File.separatorChar + DEFAULT_CONFIG_FILE;
350         }
351 
352         File file = new File(defaultConfigFile);
353         if (!file.exists()) {
354             // Then use ${jetty.home} (not supported in Jetty 10+)
355             String jettyHomeProperty = OptionHelper.getSystemProperty("jetty.home");
356             if (!OptionHelper.isEmpty(jettyHomeProperty)) {
357                 defaultConfigFile = jettyHomeProperty + File.separatorChar + DEFAULT_CONFIG_FILE;
358             } else {
359                 addInfo("Neither [jetty.base] nor [jetty.home] system properties are set.");
360             }
361         }
362 
363         file = new File(defaultConfigFile);
364         addInfo("Assuming default configuration file [" + defaultConfigFile + "]");
365         if (!file.exists()) return null;
366         return FileUtil.fileToURL(file);
367     }
368 
369     private void runJoranOnFile(URL configURL) {
370         try {
371             JoranConfigurator jc = new JoranConfigurator();
372             jc.setContext(this);
373             jc.doConfigure(configURL);
374             if (getName() == null) {
375                 setName("LogbackRequestLog");
376             }
377         } catch (JoranException e) {
378             // errors have been registered as status messages
379         }
380     }
381 
382     @Override
383     public void stop() {
384         state = State.STOPPING;
385         aai.detachAndStopAllAppenders();
386         state = State.STOPPED;
387     }
388 
389     @Override
390     public boolean isRunning() {
391         return state == State.STARTED;
392     }
393 
394     public void setFileName(String fileName) {
395         this.fileName = fileName;
396     }
397 
398     public void setResource(String resource) {
399         this.resource = resource;
400     }
401 
402     @Override
403     public boolean isStarted() {
404         return state == State.STARTED;
405     }
406 
407     @Override
408     public boolean isStarting() {
409         return state == State.STARTING;
410     }
411 
412     @Override
413     public boolean isStopping() {
414         return state == State.STOPPING;
415     }
416 
417     public boolean isStopped() {
418         return state == State.STOPPED;
419     }
420 
421     @Override
422     public boolean isFailed() {
423         return state == State.FAILED;
424     }
425 
426     @Override
427     public boolean addEventListener(EventListener listener) {
428         return false;
429     }
430 
431     @Override
432     public boolean removeEventListener(EventListener listener) {
433         return false;
434     }
435 
436 
437     public boolean isQuiet() {
438         return quiet;
439     }
440 
441     public void setQuiet(boolean quiet) {
442         this.quiet = quiet;
443     }
444 
445     @Override
446     public void addAppender(Appender<IAccessEvent> newAppender) {
447         aai.addAppender(newAppender);
448     }
449 
450     @Override
451     public Iterator<Appender<IAccessEvent>> iteratorForAppenders() {
452         return aai.iteratorForAppenders();
453     }
454 
455     @Override
456     public Appender<IAccessEvent> getAppender(String name) {
457         return aai.getAppender(name);
458     }
459 
460     @Override
461     public boolean isAttached(Appender<IAccessEvent> appender) {
462         return aai.isAttached(appender);
463     }
464 
465     @Override
466     public void detachAndStopAllAppenders() {
467         aai.detachAndStopAllAppenders();
468     }
469 
470     @Override
471     public boolean detachAppender(Appender<IAccessEvent> appender) {
472         return aai.detachAppender(appender);
473     }
474 
475     @Override
476     public boolean detachAppender(String name) {
477         return aai.detachAppender(name);
478     }
479 
480     @Override
481     public void addFilter(Filter<IAccessEvent> newFilter) {
482         fai.addFilter(newFilter);
483     }
484 
485     @Override
486     public void clearAllFilters() {
487         fai.clearAllFilters();
488     }
489 
490     @Override
491     public List<Filter<IAccessEvent>> getCopyOfAttachedFiltersList() {
492         return fai.getCopyOfAttachedFiltersList();
493     }
494 
495     @Override
496     public FilterReply getFilterChainDecision(IAccessEvent event) {
497         return fai.getFilterChainDecision(event);
498     }
499 
500     public void addLifeCycleListener(LifeCycle.Listener listener) {
501         // we'll implement this when asked
502     }
503 
504     public void removeLifeCycleListener(LifeCycle.Listener listener) {
505         // we'll implement this when asked
506     }
507 }