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 IAccessEvent accessEvent = new AccessEvent(this, jettyRequest, jettyResponse, adapter); 256 if (getFilterChainDecision(accessEvent) == FilterReply.DENY) { 257 return; 258 } 259 aai.appendLoopOnAppenders(accessEvent); 260 } 261 262 private JettyServerAdapter makeJettyServerAdapter(Request jettyRequest, Response jettyResponse) { 263 return new JettyModernServerAdapter(jettyRequest, jettyResponse); 264 } 265 266 protected void addInfo(String msg) { 267 getStatusManager().add(new InfoStatus(msg, this)); 268 } 269 270 private void addError(String msg) { 271 getStatusManager().add(new ErrorStatus(msg, this)); 272 } 273 274 @Override 275 public void start() { 276 state = State.STARTING; 277 try { 278 configure(); 279 if (!isQuiet()) { 280 StatusPrinter.print(getStatusManager()); 281 } 282 state = State.STARTED; 283 } catch (Throwable t) { 284 t.printStackTrace(); 285 state = State.FAILED; 286 } 287 } 288 289 protected void configure() { 290 URL configURL = getConfigurationFileURL(); 291 if (configURL != null) { 292 runJoranOnFile(configURL); 293 } else { 294 addError("Could not find configuration file for logback-access"); 295 } 296 } 297 298 protected URL getConfigurationFileURL() { 299 if (fileName != null) { 300 addInfo("Will use configuration file [" + fileName + "]"); 301 File file = new File(fileName); 302 if (!file.exists()) return null; 303 return FileUtil.fileToURL(file); 304 } 305 if (resource != null) { 306 addInfo("Will use configuration resource [" + resource + "]"); 307 return this.getClass().getResource(resource); 308 } 309 310 String defaultConfigFile = DEFAULT_CONFIG_FILE; 311 // Always attempt ${jetty.base} first 312 String jettyBaseProperty = OptionHelper.getSystemProperty("jetty.base"); 313 if (!OptionHelper.isNullOrEmpty(jettyBaseProperty)) { 314 defaultConfigFile = jettyBaseProperty + File.separatorChar + DEFAULT_CONFIG_FILE; 315 } 316 317 File file = new File(defaultConfigFile); 318 if (!file.exists()) { 319 // Then use ${jetty.home} (not supported in Jetty 10+) 320 String jettyHomeProperty = OptionHelper.getSystemProperty("jetty.home"); 321 if (!OptionHelper.isEmpty(jettyHomeProperty)) { 322 defaultConfigFile = jettyHomeProperty + File.separatorChar + DEFAULT_CONFIG_FILE; 323 } else { 324 addInfo("Neither [jetty.base] nor [jetty.home] system properties are set."); 325 } 326 } 327 328 file = new File(defaultConfigFile); 329 addInfo("Assuming default configuration file [" + defaultConfigFile + "]"); 330 if (!file.exists()) return null; 331 return FileUtil.fileToURL(file); 332 } 333 334 private void runJoranOnFile(URL configURL) { 335 try { 336 JoranConfigurator jc = new JoranConfigurator(); 337 jc.setContext(this); 338 jc.doConfigure(configURL); 339 if (getName() == null) { 340 setName("LogbackRequestLog"); 341 } 342 } catch (JoranException e) { 343 // errors have been registered as status messages 344 } 345 } 346 347 @Override 348 public void stop() { 349 state = State.STOPPING; 350 aai.detachAndStopAllAppenders(); 351 state = State.STOPPED; 352 } 353 354 @Override 355 public boolean isRunning() { 356 return state == State.STARTED; 357 } 358 359 public void setFileName(String fileName) { 360 this.fileName = fileName; 361 } 362 363 public void setResource(String resource) { 364 this.resource = resource; 365 } 366 367 @Override 368 public boolean isStarted() { 369 return state == State.STARTED; 370 } 371 372 @Override 373 public boolean isStarting() { 374 return state == State.STARTING; 375 } 376 377 @Override 378 public boolean isStopping() { 379 return state == State.STOPPING; 380 } 381 382 public boolean isStopped() { 383 return state == State.STOPPED; 384 } 385 386 @Override 387 public boolean isFailed() { 388 return state == State.FAILED; 389 } 390 391 @Override 392 public boolean addEventListener(EventListener listener) { 393 return false; 394 } 395 396 @Override 397 public boolean removeEventListener(EventListener listener) { 398 return false; 399 } 400 401 402 public boolean isQuiet() { 403 return quiet; 404 } 405 406 public void setQuiet(boolean quiet) { 407 this.quiet = quiet; 408 } 409 410 @Override 411 public void addAppender(Appender<IAccessEvent> newAppender) { 412 aai.addAppender(newAppender); 413 } 414 415 @Override 416 public Iterator<Appender<IAccessEvent>> iteratorForAppenders() { 417 return aai.iteratorForAppenders(); 418 } 419 420 @Override 421 public Appender<IAccessEvent> getAppender(String name) { 422 return aai.getAppender(name); 423 } 424 425 @Override 426 public boolean isAttached(Appender<IAccessEvent> appender) { 427 return aai.isAttached(appender); 428 } 429 430 @Override 431 public void detachAndStopAllAppenders() { 432 aai.detachAndStopAllAppenders(); 433 } 434 435 @Override 436 public boolean detachAppender(Appender<IAccessEvent> appender) { 437 return aai.detachAppender(appender); 438 } 439 440 @Override 441 public boolean detachAppender(String name) { 442 return aai.detachAppender(name); 443 } 444 445 @Override 446 public void addFilter(Filter<IAccessEvent> newFilter) { 447 fai.addFilter(newFilter); 448 } 449 450 @Override 451 public void clearAllFilters() { 452 fai.clearAllFilters(); 453 } 454 455 @Override 456 public List<Filter<IAccessEvent>> getCopyOfAttachedFiltersList() { 457 return fai.getCopyOfAttachedFiltersList(); 458 } 459 460 @Override 461 public FilterReply getFilterChainDecision(IAccessEvent event) { 462 return fai.getFilterChainDecision(event); 463 } 464 465 public void addLifeCycleListener(LifeCycle.Listener listener) { 466 // we'll implement this when asked 467 } 468 469 public void removeLifeCycleListener(LifeCycle.Listener listener) { 470 // we'll implement this when asked 471 } 472}