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 * <?xml version="1.0"?> 097 * <!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"> 098 * 099 * <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 */ 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}