001/*
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2026, QOS.ch. All rights reserved.
004 *
005 * This program and the accompanying materials are dual-licensed under
006 * either the terms of the Eclipse Public License v2.0 as published by
007 * the Eclipse Foundation
008 *
009 *   or (per the licensee's choosing)
010 *
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 ch.qos.logback.access.common.AccessConstants;
017import ch.qos.logback.access.common.joran.JoranConfigurator;
018import ch.qos.logback.access.common.spi.AccessEvent;
019import ch.qos.logback.access.common.spi.IAccessEvent;
020import ch.qos.logback.access.common.util.AccessCommonVersionUtil;
021import ch.qos.logback.core.Appender;
022import ch.qos.logback.core.ContextBase;
023import ch.qos.logback.core.CoreConstants;
024import ch.qos.logback.core.boolex.EventEvaluator;
025import ch.qos.logback.core.filter.Filter;
026import ch.qos.logback.core.joran.spi.JoranException;
027import ch.qos.logback.core.spi.AppenderAttachable;
028import ch.qos.logback.core.spi.AppenderAttachableImpl;
029import ch.qos.logback.core.spi.FilterAttachable;
030import ch.qos.logback.core.spi.FilterAttachableImpl;
031import ch.qos.logback.core.spi.FilterReply;
032import ch.qos.logback.core.status.ErrorStatus;
033import ch.qos.logback.core.status.InfoStatus;
034import ch.qos.logback.core.status.WarnStatus;
035import ch.qos.logback.core.util.CoreVersionUtil;
036import ch.qos.logback.core.util.FileUtil;
037import ch.qos.logback.core.util.OptionHelper;
038import ch.qos.logback.core.util.StatusPrinter;
039import ch.qos.logback.core.util.VersionUtil;
040import org.eclipse.jetty.server.Request;
041import org.eclipse.jetty.server.RequestLog;
042import org.eclipse.jetty.server.Response;
043import org.eclipse.jetty.util.component.LifeCycle;
044
045import java.io.File;
046import java.net.URL;
047import java.util.EventListener;
048import java.util.HashMap;
049import java.util.Iterator;
050import java.util.List;
051
052import static ch.qos.logback.core.CoreConstants.DEFAULT_CONTEXT_NAME;
053
054/**
055 * This class is logback's implementation of jetty's RequestLog interface.
056 * <p>
057 * It can be seen as logback classic's LoggerContext. Appenders can be attached
058 * directly to RequestLogImpl and RequestLogImpl uses the same StatusManager as
059 * LoggerContext does. It also provides containers for properties.
060 *
061 * </p>
062 * <h2>Supported Jetty Versions</h2>
063 * <p>
064 * This {@code RequestLogImpl} only supports Jetty 7.0.0 through Jetty 10.
065 * If you are using Jetty 11 with the new Jakarta Servlets (namespace {@code jakarta.servlet})
066 * then you will need a more modern version of {@code logback-access}.
067 * </p>
068 * <h2>Configuring for Jetty 9.4.x through to Jetty 10.0.x</h2>
069 * <p>
070 * Jetty 9.4.x and Jetty 10.x use a modern {@code org.eclipse.jetty.server.Server.setRequestLog(RequestLog)}
071 * interface that is based on a Server level RequestLog behavior.  This means all requests are logged,
072 * even bad requests, and context-less requests.
073 * </p>
074 * <p>
075 * The internals of the Jetty Request and Response objects track the state of the object at the time
076 * they are committed (the actual state during the application when an action on the network commits the
077 * request/response exchange).  This prevents behaviors from 3rd party libraries
078 * that change the state of the request / response before the RequestLog gets a chance
079 * to log the details.  This differs from Jetty 9.3.x and
080 * older in that those versions used a (now deprecated) {@code RequestLogHandler} and
081 * would never see bad requests, or context-less requests,
082 * and if a 3rd party library modifies the the response (for example by setting
083 * {@code response.setStatus(200)} after the response has been initiated on the network)
084 * this change in status would be logged, instead of the actual status that was sent.
085 * </p>
086 * <p>
087 * First, you must be using the proper {@code ${jetty.home}} and {@code ${jetty.base}}
088 * directory split.  Configure your {@code ${jetty.base}} with at least the `resources` module
089 * enabled (so that your configuration can be found).
090 * </p>
091 * <p>
092 * Next, create a {@code ${jetty.base}/etc/logback-access.xml} file with the following
093 * content.
094 * </p>
095 * <pre>
096 *   &lt;?xml version="1.0"?&gt;
097 *   &lt;!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"&gt;
098 *
099 *   &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 */
238public 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            String accessJetty12Version = Jetty12VersionUtil.getAccessJetty12VersionBySelfDeclaredProperties();
315            VersionUtil.checkForVersionEquality(this, accessJetty12Version, accessCommonVersion, LOGBACK_ACCESS_JETTY12_NAME, LOGBACK_ACCESS_COMMON_NAME);
316            AccessCommonVersionUtil accessCommonVersionUtil = new AccessCommonVersionUtil(this);
317            accessCommonVersionUtil.compareExpectedAndFoundVersion( coreVersion, AccessConstants.class, accessCommonVersion, LOGBACK_ACCESS_COMMON_NAME, LOGBACK_CORE_NAME);
318        } catch(NoClassDefFoundError e) {
319            addWarn("Missing ch.logback.core.util.VersionUtil class on classpath. The version of logback-core is probably earlier than 1.5.25.");
320        } catch(NoSuchMethodError e) {
321            addWarn(e.getMessage() + ". The version of logback-core is probably earlier than 1.5.26.");
322        }
323    }
324
325
326    protected void configure() {
327        URL configURL = getConfigurationFileURL();
328        if (configURL != null) {
329            runJoranOnFile(configURL);
330        } else {
331            addError("Could not find configuration file for logback-access");
332        }
333    }
334
335    protected URL getConfigurationFileURL() {
336        if (fileName != null) {
337            addInfo("Will use configuration file [" + fileName + "]");
338            File file = new File(fileName);
339            if (!file.exists()) return null;
340            return FileUtil.fileToURL(file);
341        }
342        if (resource != null) {
343            addInfo("Will use configuration resource [" + resource + "]");
344            return this.getClass().getResource(resource);
345        }
346
347        String defaultConfigFile = DEFAULT_CONFIG_FILE;
348        // Always attempt ${jetty.base} first
349        String jettyBaseProperty = OptionHelper.getSystemProperty("jetty.base");
350        if (!OptionHelper.isNullOrEmpty(jettyBaseProperty)) {
351            defaultConfigFile = jettyBaseProperty + File.separatorChar + DEFAULT_CONFIG_FILE;
352        }
353
354        File file = new File(defaultConfigFile);
355        if (!file.exists()) {
356            // Then use ${jetty.home} (not supported in Jetty 10+)
357            String jettyHomeProperty = OptionHelper.getSystemProperty("jetty.home");
358            if (!OptionHelper.isEmpty(jettyHomeProperty)) {
359                defaultConfigFile = jettyHomeProperty + File.separatorChar + DEFAULT_CONFIG_FILE;
360            } else {
361                addInfo("Neither [jetty.base] nor [jetty.home] system properties are set.");
362            }
363        }
364
365        file = new File(defaultConfigFile);
366        addInfo("Assuming default configuration file [" + defaultConfigFile + "]");
367        if (!file.exists()) return null;
368        return FileUtil.fileToURL(file);
369    }
370
371    private void runJoranOnFile(URL configURL) {
372        try {
373            JoranConfigurator jc = new JoranConfigurator();
374            jc.setContext(this);
375            jc.doConfigure(configURL);
376            if (getName() == null) {
377                setName("LogbackRequestLog");
378            }
379        } catch (JoranException e) {
380            // errors have been registered as status messages
381        }
382    }
383
384    @Override
385    public void stop() {
386        state = State.STOPPING;
387        aai.detachAndStopAllAppenders();
388        state = State.STOPPED;
389    }
390
391    @Override
392    public boolean isRunning() {
393        return state == State.STARTED;
394    }
395
396    public void setFileName(String fileName) {
397        this.fileName = fileName;
398    }
399
400    public void setResource(String resource) {
401        this.resource = resource;
402    }
403
404    @Override
405    public boolean isStarted() {
406        return state == State.STARTED;
407    }
408
409    @Override
410    public boolean isStarting() {
411        return state == State.STARTING;
412    }
413
414    @Override
415    public boolean isStopping() {
416        return state == State.STOPPING;
417    }
418
419    public boolean isStopped() {
420        return state == State.STOPPED;
421    }
422
423    @Override
424    public boolean isFailed() {
425        return state == State.FAILED;
426    }
427
428    @Override
429    public boolean addEventListener(EventListener listener) {
430        return false;
431    }
432
433    @Override
434    public boolean removeEventListener(EventListener listener) {
435        return false;
436    }
437
438
439    public boolean isQuiet() {
440        return quiet;
441    }
442
443    public void setQuiet(boolean quiet) {
444        this.quiet = quiet;
445    }
446
447    @Override
448    public void addAppender(Appender<IAccessEvent> newAppender) {
449        aai.addAppender(newAppender);
450    }
451
452    @Override
453    public Iterator<Appender<IAccessEvent>> iteratorForAppenders() {
454        return aai.iteratorForAppenders();
455    }
456
457    @Override
458    public Appender<IAccessEvent> getAppender(String name) {
459        return aai.getAppender(name);
460    }
461
462    @Override
463    public boolean isAttached(Appender<IAccessEvent> appender) {
464        return aai.isAttached(appender);
465    }
466
467    @Override
468    public void detachAndStopAllAppenders() {
469        aai.detachAndStopAllAppenders();
470    }
471
472    @Override
473    public boolean detachAppender(Appender<IAccessEvent> appender) {
474        return aai.detachAppender(appender);
475    }
476
477    @Override
478    public boolean detachAppender(String name) {
479        return aai.detachAppender(name);
480    }
481
482    @Override
483    public void addFilter(Filter<IAccessEvent> newFilter) {
484        fai.addFilter(newFilter);
485    }
486
487    @Override
488    public void clearAllFilters() {
489        fai.clearAllFilters();
490    }
491
492    @Override
493    public List<Filter<IAccessEvent>> getCopyOfAttachedFiltersList() {
494        return fai.getCopyOfAttachedFiltersList();
495    }
496
497    @Override
498    public FilterReply getFilterChainDecision(IAccessEvent event) {
499        return fai.getFilterChainDecision(event);
500    }
501
502    public void addLifeCycleListener(LifeCycle.Listener listener) {
503        // we'll implement this when asked
504    }
505
506    public void removeLifeCycleListener(LifeCycle.Listener listener) {
507        // we'll implement this when asked
508    }
509}