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.tomcat; 015 016import java.io.File; 017import java.io.IOException; 018import java.net.MalformedURLException; 019import java.net.URL; 020import java.util.ArrayList; 021import java.util.HashMap; 022import java.util.Iterator; 023import java.util.List; 024import java.util.Map; 025import java.util.concurrent.ExecutorService; 026import java.util.concurrent.ScheduledExecutorService; 027import java.util.concurrent.ScheduledFuture; 028import java.util.concurrent.locks.ReentrantLock; 029 030import ch.qos.logback.access.common.AccessConstants; 031import ch.qos.logback.access.common.joran.JoranConfigurator; 032import ch.qos.logback.access.common.spi.AccessEvent; 033import ch.qos.logback.access.common.spi.IAccessEvent; 034import ch.qos.logback.access.common.util.AccessCommonVersionUtil; 035import ch.qos.logback.core.spi.ConfigurationEvent; 036import ch.qos.logback.core.spi.ConfigurationEventListener; 037import ch.qos.logback.core.util.CoreVersionUtil; 038import ch.qos.logback.core.util.VersionUtil; 039import jakarta.servlet.ServletContext; 040import jakarta.servlet.ServletException; 041 042import org.apache.catalina.Lifecycle; 043import org.apache.catalina.LifecycleException; 044import org.apache.catalina.LifecycleListener; 045import org.apache.catalina.LifecycleState; 046import org.apache.catalina.connector.Request; 047import org.apache.catalina.connector.Response; 048import org.apache.catalina.valves.ValveBase; 049 050import ch.qos.logback.core.Appender; 051import ch.qos.logback.core.BasicStatusManager; 052import ch.qos.logback.core.Context; 053import ch.qos.logback.core.CoreConstants; 054import ch.qos.logback.core.LifeCycleManager; 055import ch.qos.logback.core.boolex.EventEvaluator; 056import ch.qos.logback.core.filter.Filter; 057import ch.qos.logback.core.joran.spi.JoranException; 058import ch.qos.logback.core.spi.AppenderAttachable; 059import ch.qos.logback.core.spi.AppenderAttachableImpl; 060import ch.qos.logback.core.spi.FilterAttachable; 061import ch.qos.logback.core.spi.FilterAttachableImpl; 062import ch.qos.logback.core.spi.FilterReply; 063import ch.qos.logback.core.spi.LifeCycle; 064import ch.qos.logback.core.spi.LogbackLock; 065import ch.qos.logback.core.spi.SequenceNumberGenerator; 066import ch.qos.logback.core.status.ErrorStatus; 067import ch.qos.logback.core.status.InfoStatus; 068import ch.qos.logback.core.status.OnConsoleStatusListener; 069import ch.qos.logback.core.status.Status; 070import ch.qos.logback.core.status.StatusManager; 071import ch.qos.logback.core.status.WarnStatus; 072import ch.qos.logback.core.util.ExecutorServiceUtil; 073import ch.qos.logback.core.util.Loader; 074import ch.qos.logback.core.util.OptionHelper; 075import ch.qos.logback.core.util.StatusListenerConfigHelper; 076 077import static ch.qos.logback.access.tomcat.AccessTomcatConstants.LOGBACK_ACCESS_TOMCAT_MODULE_NAME; 078 079//import org.apache.catalina.Lifecycle; 080 081/** 082 * This class is an implementation of tomcat's Valve interface, by extending 083 * ValveBase. 084 * 085 * <p> 086 * For more information on using LogbackValve please refer to the online 087 * documentation on 088 * <a href="http://logback.qos.ch/access.html#tomcat">logback-access and 089 * tomcat</a>. 090 * 091 * @author Ceki Gülcü 092 * @author Sébastien Pennec 093 */ 094public class LogbackValve extends ValveBase 095 implements Lifecycle, Context, AppenderAttachable<IAccessEvent>, FilterAttachable<IAccessEvent> { 096 097 public final static String DEFAULT_FILENAME = "logback-access.xml"; 098 public final static String DEFAULT_CONFIG_FILE = "conf" + File.separatorChar + DEFAULT_FILENAME; 099 final static String CATALINA_BASE_KEY = "catalina.base"; 100 final static String CATALINA_HOME_KEY = "catalina.home"; 101 102 private final LifeCycleManager lifeCycleManager = new LifeCycleManager(); 103 104 private long birthTime = System.currentTimeMillis(); 105 106 ReentrantLock configurationLock = new ReentrantLock(); 107 108 final private List<ConfigurationEventListener> configurationEventListenerList = new ArrayList<>(); 109 110 // Attributes from ContextBase: 111 private String name; 112 StatusManager sm = new BasicStatusManager(); 113 // TODO propertyMap should be observable so that we can be notified 114 // when it changes so that a new instance of propertyMap can be 115 // serialized. For the time being, we ignore this shortcoming. 116 Map<String, String> propertyMap = new HashMap<String, String>(); 117 Map<String, Object> objectMap = new HashMap<String, Object>(); 118 private FilterAttachableImpl<IAccessEvent> fai = new FilterAttachableImpl<IAccessEvent>(); 119 120 AppenderAttachableImpl<IAccessEvent> aai = new AppenderAttachableImpl<IAccessEvent>(); 121 String filenameOption; 122 boolean quiet; 123 boolean started; 124 boolean alreadySetLogbackStatusManager = false; 125 private SequenceNumberGenerator sequenceNumberGenerator; 126 127 private ScheduledExecutorService scheduledExecutorService; 128 129 public LogbackValve() { 130 putObject(CoreConstants.EVALUATOR_MAP, new HashMap<String, EventEvaluator<?>>()); 131 } 132 133 public boolean isStarted() { 134 return started; 135 } 136 137 @Override 138 public void startInternal() throws LifecycleException { 139 140 versionCheck(); 141 142 scheduledExecutorService = ExecutorServiceUtil.newScheduledExecutorService(); 143 144 String filename; 145 146 if (filenameOption != null) { 147 filename = filenameOption; 148 } else { 149 addInfo("filename property not set. Assuming [" + DEFAULT_CONFIG_FILE + "]"); 150 filename = DEFAULT_CONFIG_FILE; 151 } 152 153 // String catalinaBase = OptionHelper.getSystemProperty(CATALINA_BASE_KEY); 154 // String catalinaHome = OptionHelper.getSystemProperty(CATALINA_BASE_KEY); 155 156 File configFile = searchForConfigFileTomcatProperty(filename, CATALINA_BASE_KEY); 157 if (configFile == null) { 158 configFile = searchForConfigFileTomcatProperty(filename, CATALINA_HOME_KEY); 159 } 160 161 URL resourceURL; 162 if (configFile != null) 163 resourceURL = fileToUrl(configFile); 164 else 165 resourceURL = searchAsResource(filename); 166 167 if (resourceURL != null) { 168 configureAsResource(resourceURL); 169 } else { 170 addWarn("Failed to find valid logback-access configuration file."); 171 } 172 173 if (!quiet) { 174 StatusListenerConfigHelper.addOnConsoleListenerInstance(this, new OnConsoleStatusListener()); 175 } 176 177 started = true; 178 setState(LifecycleState.STARTING); 179 } 180 181 static String LOGBACK_ACCESS_COMMON_NAME = "logback-access-common"; 182 static String LOGBACK_CORE_NAME = "logback-core"; 183 184 private void versionCheck() { 185 try { 186 String coreVersion = CoreVersionUtil.getCoreVersionBySelfDeclaredProperties(); 187 String accessCommonVersion = AccessCommonVersionUtil.getAccessCommonVersionBySelfDeclaredProperties(); 188 String tomcatVersion = AccessTomcatVersionUtil.getCoreVersionBySelfDeclaredProperties(); 189 VersionUtil.checkForVersionEquality(this, tomcatVersion, accessCommonVersion, LOGBACK_ACCESS_TOMCAT_MODULE_NAME, LOGBACK_ACCESS_COMMON_NAME); 190 AccessCommonVersionUtil accessCommonVersionUtil = new AccessCommonVersionUtil(this); 191 accessCommonVersionUtil.compareExpectedAndFoundVersion(coreVersion, AccessConstants.class, accessCommonVersion, LOGBACK_ACCESS_COMMON_NAME, LOGBACK_CORE_NAME); 192 } catch(NoClassDefFoundError e) { 193 addWarn("Missing ch.logback.core.util.VersionUtil class on classpath. The version of logback-core is probably earlier than 1.5.25."); 194 } catch(NoSuchMethodError e) { 195 addWarn(e.getMessage() + ". The version of logback-core is probably earlier than 1.5.26."); 196 } 197 } 198 199 private URL fileToUrl(File configFile) { 200 try { 201 return configFile.toURI().toURL(); 202 } catch (MalformedURLException e) { 203 throw new IllegalStateException("File to URL conversion failed", e); 204 } 205 } 206 207 private URL searchAsResource(String filename) { 208 URL result = Loader.getResource(filename, getClass().getClassLoader()); 209 if (result != null) 210 addInfo("Found [" + filename + "] as a resource."); 211 else 212 addInfo("Could NOT find [" + filename + "] as a resource."); 213 return result; 214 } 215 216 private File searchForConfigFileTomcatProperty(String filename, String propertyKey) { 217 String propertyValue = OptionHelper.getSystemProperty(propertyKey); 218 String candidatePath = propertyValue + File.separatorChar + filename; 219 if (propertyValue == null) { 220 addInfo("System property \"" + propertyKey + "\" is not set. Skipping configuration file search with ${" 221 + propertyKey + "} path prefix."); 222 return null; 223 } 224 File candidateFile = new File(candidatePath); 225 if (candidateFile.exists()) { 226 addInfo("Found configuration file [" + candidatePath + "] using property \"" + propertyKey + "\""); 227 return candidateFile; 228 } else { 229 addInfo("Could NOT find configuration file [" + candidatePath + "] using property \"" + propertyKey + "\""); 230 return null; 231 } 232 } 233 234 public void addStatus(Status status) { 235 StatusManager sm = getStatusManager(); 236 if (sm != null) { 237 sm.add(status); 238 } 239 } 240 241 public void addInfo(String msg) { 242 addStatus(new InfoStatus(msg, this)); 243 } 244 245 public void addWarn(String msg) { 246 addStatus(new WarnStatus(msg, this)); 247 } 248 249 public void addError(String msg, Throwable t) { 250 addStatus(new ErrorStatus(msg, this, t)); 251 } 252 253 private void configureAsResource(URL resourceURL) { 254 try { 255 JoranConfigurator jc = new JoranConfigurator(); 256 jc.setContext(this); 257 jc.doConfigure(resourceURL); 258 addInfo("Done configuring"); 259 } catch (JoranException e) { 260 addError("Failed to configure LogbackValve", e); 261 } 262 } 263 264 public String getFilename() { 265 return filenameOption; 266 } 267 268 public void setFilename(String filename) { 269 this.filenameOption = filename; 270 } 271 272 public boolean isQuiet() { 273 return quiet; 274 } 275 276 public void setQuiet(boolean quiet) { 277 this.quiet = quiet; 278 } 279 280 @Override 281 public void invoke(Request request, Response response) throws IOException, ServletException { 282 try { 283 if (!alreadySetLogbackStatusManager) { 284 alreadySetLogbackStatusManager = true; 285 org.apache.catalina.Context tomcatContext = request.getContext(); 286 if (tomcatContext != null) { 287 ServletContext sc = tomcatContext.getServletContext(); 288 if (sc != null) { 289 sc.setAttribute(AccessConstants.LOGBACK_STATUS_MANAGER_KEY, getStatusManager()); 290 } 291 } 292 } 293 294 getNext().invoke(request, response); 295 296 TomcatServerAdapter adapter = new TomcatServerAdapter(request, response); 297 IAccessEvent accessEvent = new AccessEvent(this, request, response, adapter); 298 299 addThreadName(accessEvent); 300 301 if (getFilterChainDecision(accessEvent) == FilterReply.DENY) { 302 return; 303 } 304 305 // TODO better exception handling 306 aai.appendLoopOnAppenders(accessEvent); 307 } finally { 308 request.removeAttribute(AccessConstants.LOGBACK_STATUS_MANAGER_KEY); 309 } 310 } 311 312 private void addThreadName(IAccessEvent accessEvent) { 313 try { 314 final String threadName = Thread.currentThread().getName(); 315 if (threadName != null) { 316 accessEvent.setThreadName(threadName); 317 } 318 } catch (Exception ignored) { 319 } 320 } 321 322 @Override 323 protected void stopInternal() throws LifecycleException { 324 started = false; 325 setState(LifecycleState.STOPPING); 326 lifeCycleManager.reset(); 327 if (scheduledExecutorService != null) { 328 ExecutorServiceUtil.shutdown(scheduledExecutorService); 329 scheduledExecutorService = null; 330 } 331 } 332 333 @Override 334 public void addAppender(Appender<IAccessEvent> newAppender) { 335 aai.addAppender(newAppender); 336 } 337 338 @Override 339 public Iterator<Appender<IAccessEvent>> iteratorForAppenders() { 340 return aai.iteratorForAppenders(); 341 } 342 343 @Override 344 public Appender<IAccessEvent> getAppender(String name) { 345 return aai.getAppender(name); 346 } 347 348 @Override 349 public boolean isAttached(Appender<IAccessEvent> appender) { 350 return aai.isAttached(appender); 351 } 352 353 @Override 354 public void detachAndStopAllAppenders() { 355 aai.detachAndStopAllAppenders(); 356 357 } 358 359 @Override 360 public boolean detachAppender(Appender<IAccessEvent> appender) { 361 return aai.detachAppender(appender); 362 } 363 364 @Override 365 public boolean detachAppender(String name) { 366 return aai.detachAppender(name); 367 } 368 369 public String getInfo() { 370 return "Logback's implementation of ValveBase"; 371 } 372 373 // Methods from ContextBase: 374 @Override 375 public StatusManager getStatusManager() { 376 return sm; 377 } 378 379 public Map<String, String> getPropertyMap() { 380 return propertyMap; 381 } 382 383 @Override 384 public void addSubstitutionProperty(String key, String value) { 385 this.propertyMap.put(key, value); 386 } 387 388 @Override 389 public void putProperty(String key, String val) { 390 this.propertyMap.put(key, val); 391 } 392 393 @Override 394 public String getProperty(String key) { 395 return (String) this.propertyMap.get(key); 396 } 397 398 @Override 399 public Map<String, String> getCopyOfPropertyMap() { 400 return new HashMap<String, String>(this.propertyMap); 401 } 402 403 @Override 404 public Object getObject(String key) { 405 return objectMap.get(key); 406 } 407 408 @Override 409 public void putObject(String key, Object value) { 410 objectMap.put(key, value); 411 } 412 413 @Override 414 public void addFilter(Filter<IAccessEvent> newFilter) { 415 fai.addFilter(newFilter); 416 } 417 418 @Override 419 public void clearAllFilters() { 420 fai.clearAllFilters(); 421 } 422 423 @Override 424 public List<Filter<IAccessEvent>> getCopyOfAttachedFiltersList() { 425 return fai.getCopyOfAttachedFiltersList(); 426 } 427 428 @Override 429 public FilterReply getFilterChainDecision(IAccessEvent event) { 430 return fai.getFilterChainDecision(event); 431 } 432 433 @Override 434 public ExecutorService getExecutorService() { 435 return getScheduledExecutorService(); 436 } 437 438 @Override 439 public String getName() { 440 return name; 441 } 442 443 @Override 444 public void setName(String name) { 445 if (this.name != null) { 446 throw new IllegalStateException("LogbackValve has been already given a name"); 447 } 448 this.name = name; 449 } 450 451 @Override 452 public long getBirthTime() { 453 return birthTime; 454 } 455 456 @Override 457 public ReentrantLock getConfigurationLock() { 458 return configurationLock; 459 } 460 461 @Override 462 public void register(LifeCycle component) { 463 lifeCycleManager.register(component); 464 } 465 466 // ====== Methods from catalina Lifecycle ===== 467 468 @Override 469 public void addLifecycleListener(LifecycleListener arg0) { 470 // dummy NOP implementation 471 } 472 473 @Override 474 public LifecycleListener[] findLifecycleListeners() { 475 return new LifecycleListener[0]; 476 } 477 478 @Override 479 public void removeLifecycleListener(LifecycleListener arg0) { 480 // dummy NOP implementation 481 } 482 483 @Override 484 public String toString() { 485 StringBuilder sb = new StringBuilder(this.getClass().getName()); 486 sb.append('['); 487 sb.append(getName()); 488 sb.append(']'); 489 return sb.toString(); 490 } 491 492 @Override 493 public ScheduledExecutorService getScheduledExecutorService() { 494 return scheduledExecutorService; 495 } 496 497 @Override 498 public void addScheduledFuture(ScheduledFuture<?> scheduledFuture) { 499 throw new UnsupportedOperationException(); 500 } 501 502 public SequenceNumberGenerator getSequenceNumberGenerator() { 503 return sequenceNumberGenerator; 504 } 505 506 public void setSequenceNumberGenerator(SequenceNumberGenerator sequenceNumberGenerator) { 507 this.sequenceNumberGenerator = sequenceNumberGenerator; 508 } 509 510 511 @Override 512 public void addConfigurationEventListener(ConfigurationEventListener listener) { 513 configurationEventListenerList.add(listener); 514 } 515 516 @Override 517 public void fireConfigurationEvent(ConfigurationEvent configurationEvent) { 518 configurationEventListenerList.forEach( l -> l.listen(configurationEvent)); 519 } 520}