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.joran.JoranConfigurator; 017import ch.qos.logback.access.common.spi.AccessEvent; 018import ch.qos.logback.access.common.spi.IAccessEvent; 019import ch.qos.logback.core.Appender; 020import ch.qos.logback.core.ContextBase; 021import ch.qos.logback.core.CoreConstants; 022import ch.qos.logback.core.boolex.EventEvaluator; 023import ch.qos.logback.core.filter.Filter; 024import ch.qos.logback.core.joran.spi.JoranException; 025import ch.qos.logback.core.spi.AppenderAttachable; 026import ch.qos.logback.core.spi.AppenderAttachableImpl; 027import ch.qos.logback.core.spi.FilterAttachable; 028import ch.qos.logback.core.spi.FilterAttachableImpl; 029import ch.qos.logback.core.spi.FilterReply; 030import ch.qos.logback.core.status.ErrorStatus; 031import ch.qos.logback.core.status.InfoStatus; 032import ch.qos.logback.core.util.FileUtil; 033import ch.qos.logback.core.util.OptionHelper; 034import ch.qos.logback.core.util.StatusPrinter; 035import org.eclipse.jetty.server.Request; 036import org.eclipse.jetty.server.RequestLog; 037import org.eclipse.jetty.server.Response; 038import org.eclipse.jetty.util.component.LifeCycle; 039 040import java.io.File; 041import java.net.URL; 042import java.util.EventListener; 043import java.util.HashMap; 044import java.util.Iterator; 045import java.util.List; 046 047/** 048 * This class is logback's implementation of jetty's RequestLog interface. 049 * <p> 050 * It can be seen as logback classic's LoggerContext. Appenders can be attached 051 * directly to RequestLogImpl and RequestLogImpl uses the same StatusManager as 052 * LoggerContext does. It also provides containers for properties. 053 * 054 * </p> 055 * <h2>Supported Jetty Versions</h2> 056 * <p> 057 * This {@code RequestLogImpl} only supports Jetty 7.0.0 through Jetty 10. 058 * If you are using Jetty 11 with the new Jakarta Servlets (namespace {@code jakarta.servlet}) 059 * then you will need a more modern version of {@code logback-access}. 060 * </p> 061 * <h2>Configuring for Jetty 9.4.x through to Jetty 10.0.x</h2> 062 * <p> 063 * Jetty 9.4.x and Jetty 10.x use a modern {@code org.eclipse.jetty.server.Server.setRequestLog(RequestLog)} 064 * interface that is based on a Server level RequestLog behavior. This means all requests are logged, 065 * even bad requests, and context-less requests. 066 * </p> 067 * <p> 068 * The internals of the Jetty Request and Response objects track the state of the object at the time 069 * they are committed (the actual state during the application when an action on the network commits the 070 * request/response exchange). This prevents behaviors from 3rd party libraries 071 * that change the state of the request / response before the RequestLog gets a chance 072 * to log the details. This differs from Jetty 9.3.x and 073 * older in that those versions used a (now deprecated) {@code RequestLogHandler} and 074 * would never see bad requests, or context-less requests, 075 * and if a 3rd party library modifies the the response (for example by setting 076 * {@code response.setStatus(200)} after the response has been initiated on the network) 077 * this change in status would be logged, instead of the actual status that was sent. 078 * </p> 079 * <p> 080 * First, you must be using the proper {@code ${jetty.home}} and {@code ${jetty.base}} 081 * directory split. Configure your {@code ${jetty.base}} with at least the `resources` module 082 * enabled (so that your configuration can be found). 083 * </p> 084 * <p> 085 * Next, create a {@code ${jetty.base}/etc/logback-access.xml} file with the following 086 * content. 087 * </p> 088 * <pre> 089 * <?xml version="1.0"?> 090 * <!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd"> 091 * 092 * <Configure id="Server" class="org.eclipse.jetty.server.Server"> 093 * <Set name="requestLog"> 094 * <New id="LogbackAccess" class="ch.qos.logback.access.jetty.RequestLogImpl"> 095 * <Set name="resource">logback-access.xml</Set> 096 * </New> 097 * </Set> 098 * </Configure></pre> 099 * 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 */ 231public 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}