001/** 002 * Logback: the reliable, generic, fast and flexible logging framework. 003 * Copyright (C) 1999-2015, QOS.ch. All rights reserved. 004 * <p> 005 * This program and the accompanying materials are dual-licensed under 006 * either the terms of the Eclipse Public License v1.0 as published by 007 * the Eclipse Foundation 008 * <p> 009 * or (per the licensee's choosing) 010 * <p> 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 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}