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 * <?xml version="1.0"?>
90 * <!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
91 *
92 * <Configure id="Server" class="org.eclipse.jetty.server.Server">
93 * <Set name="requestLog">
94 * <New id="LogbackAccess" class="ch.qos.logback.access.jetty.RequestLogImpl">
95 * <Set name="resource">logback-access.xml</Set>
96 * </New>
97 * </Set>
98 * </Configure></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 * <?xml version="1.0"?>
123 * <!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
124 *
125 * <Configure id="Server" class="org.eclipse.jetty.server.Server">
126 * <Set name="requestLog">
127 * <New id="LogbackAccess" class="ch.qos.logback.access.jetty.RequestLogImpl">
128 * <Set name="fileName">/arbitrary/path/to/logback-access.xml</Set>
129 * </New>
130 * </Set>
131 * </Configure>
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 * <?xml version="1.0"?>
142 * <!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
143 *
144 * <Configure id="Server" class="org.eclipse.jetty.server.Server">
145 * <Ref id="Handlers">
146 * <Call name="addHandler">
147 * <Arg>
148 * <New id="RequestLog" class="org.eclipse.jetty.server.handler.RequestLogHandler">
149 * <Set name="requestLog">
150 * <New id="RequestLogImpl" class="ch.qos.logback.access.jetty.RequestLogImpl"/>
151 * </Set>
152 * </New>
153 * </Arg>
154 * </Call>
155 * </Ref>
156 * </Configure>
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 * <?xml version="1.0"?>
175 * <!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
176 *
177 * <Configure id="Server" class="org.eclipse.jetty.server.Server">
178 * <Ref id="Handlers">
179 * <Call name="addHandler">
180 * <Arg>
181 * <New id="RequestLog" class="org.eclipse.jetty.server.handler.RequestLogHandler">
182 * <Set name="requestLog">
183 * <New id="RequestLogImpl" class="ch.qos.logback.access.jetty.RequestLogImpl">
184 * <Set name="fileName">path/to/logback-access.xml</Set>
185 * </New>
186 * </Set>
187 * </New>
188 * </Arg>
189 * </Call>
190 * </Ref>
191 * </Configure>
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 * <configuration>
199 * <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
200 * <layout class="ch.qos.logback.access.PatternLayout">
201 * <param name="Pattern" value="%date %server %remoteIP %clientHost %user %requestURL" />
202 * </layout>
203 * </appender>
204 *
205 * <appender-ref ref="STDOUT" />
206 * </configuration>
207 * </pre>
208 * <p>
209 * Here is another configuration file, using SMTPAppender:
210 * <p/>
211 *
212 * <pre>
213 * <configuration>
214 * <appender name="SMTP" class="ch.qos.logback.access.net.SMTPAppender">
215 * <layout class="ch.qos.logback.access.PatternLayout">
216 * <param name="pattern" value="%remoteIP [%date] %requestURL %statusCode %bytesSent" />
217 * </layout>
218 * <param name="From" value="sender@domaine.org" />
219 * <param name="SMTPHost" value="mail.domain.org" />
220 * <param name="Subject" value="Last Event: %statusCode %requestURL" />
221 * <param name="To" value="server_admin@domain.org" />
222 * </appender>
223 * <appender-ref ref="SMTP" />
224 * </configuration>
225 * </pre>
226 *
227 * @author Ceki Gülcü
228 * @author Sé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 }