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 * <?xml version="1.0"?>
97 * <!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
98 *
99 * <Configure id="Server" class="org.eclipse.jetty.server.Server">
100 * <Set name="requestLog">
101 * <New id="LogbackAccess" class="ch.qos.logback.access.jetty.RequestLogImpl">
102 * <Set name="resource">logback-access.xml</Set>
103 * </New>
104 * </Set>
105 * </Configure></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 * <?xml version="1.0"?>
130 * <!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
131 *
132 * <Configure id="Server" class="org.eclipse.jetty.server.Server">
133 * <Set name="requestLog">
134 * <New id="LogbackAccess" class="ch.qos.logback.access.jetty.RequestLogImpl">
135 * <Set name="fileName">/arbitrary/path/to/logback-access.xml</Set>
136 * </New>
137 * </Set>
138 * </Configure>
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 * <?xml version="1.0"?>
149 * <!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
150 *
151 * <Configure id="Server" class="org.eclipse.jetty.server.Server">
152 * <Ref id="Handlers">
153 * <Call name="addHandler">
154 * <Arg>
155 * <New id="RequestLog" class="org.eclipse.jetty.server.handler.RequestLogHandler">
156 * <Set name="requestLog">
157 * <New id="RequestLogImpl" class="ch.qos.logback.access.jetty.RequestLogImpl"/>
158 * </Set>
159 * </New>
160 * </Arg>
161 * </Call>
162 * </Ref>
163 * </Configure>
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 * <?xml version="1.0"?>
182 * <!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
183 *
184 * <Configure id="Server" class="org.eclipse.jetty.server.Server">
185 * <Ref id="Handlers">
186 * <Call name="addHandler">
187 * <Arg>
188 * <New id="RequestLog" class="org.eclipse.jetty.server.handler.RequestLogHandler">
189 * <Set name="requestLog">
190 * <New id="RequestLogImpl" class="ch.qos.logback.access.jetty.RequestLogImpl">
191 * <Set name="fileName">path/to/logback-access.xml</Set>
192 * </New>
193 * </Set>
194 * </New>
195 * </Arg>
196 * </Call>
197 * </Ref>
198 * </Configure>
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 * <configuration>
206 * <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
207 * <layout class="ch.qos.logback.access.PatternLayout">
208 * <param name="Pattern" value="%date %server %remoteIP %clientHost %user %requestURL" />
209 * </layout>
210 * </appender>
211 *
212 * <appender-ref ref="STDOUT" />
213 * </configuration>
214 * </pre>
215 * <p>
216 * Here is another configuration file, using SMTPAppender:
217 * <p/>
218 *
219 * <pre>
220 * <configuration>
221 * <appender name="SMTP" class="ch.qos.logback.access.net.SMTPAppender">
222 * <layout class="ch.qos.logback.access.PatternLayout">
223 * <param name="pattern" value="%remoteIP [%date] %requestURL %statusCode %bytesSent" />
224 * </layout>
225 * <param name="From" value="sender@domaine.org" />
226 * <param name="SMTPHost" value="mail.domain.org" />
227 * <param name="Subject" value="Last Event: %statusCode %requestURL" />
228 * <param name="To" value="server_admin@domain.org" />
229 * </appender>
230 * <appender-ref ref="SMTP" />
231 * </configuration>
232 * </pre>
233 *
234 * @author Ceki Gülcü
235 * @author Sé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 }