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.classic.model.processor; 015 016import ch.qos.logback.classic.joran.ReconfigureOnChangeTask; 017import ch.qos.logback.classic.model.ConfigurationModel; 018import ch.qos.logback.core.Context; 019import ch.qos.logback.core.CoreConstants; 020import ch.qos.logback.core.joran.spi.ConfigurationWatchList; 021import ch.qos.logback.core.joran.util.ConfigurationWatchListUtil; 022import ch.qos.logback.core.model.Model; 023import ch.qos.logback.core.model.processor.ModelHandlerBase; 024import ch.qos.logback.core.model.processor.ModelHandlerException; 025import ch.qos.logback.core.model.processor.ModelInterpretationContext; 026import ch.qos.logback.core.spi.ConfigurationEvent; 027import ch.qos.logback.core.util.Duration; 028import ch.qos.logback.core.util.OptionHelper; 029 030import java.util.concurrent.ScheduledExecutorService; 031import java.util.concurrent.ScheduledFuture; 032import java.util.concurrent.TimeUnit; 033 034/** 035 * This is a subclass of {@link ConfigurationModelHandler} offering configuration reloading support. 036 * 037 * <p>This class is also called by logback-tyler.</p> 038 * 039 */ 040public class ConfigurationModelHandlerFull extends ConfigurationModelHandler { 041 042 public static final String FAILED_WATCH_PREDICATE_MESSAGE_1 = "Missing watchable .xml or .properties files."; 043 public static final String FAILED_WATCH_PREDICATE_MESSAGE_2 = "Watching .xml files requires that the main configuration file is reachable as a URL"; 044 045 public ConfigurationModelHandlerFull(Context context) { 046 super(context); 047 } 048 049 static public ModelHandlerBase makeInstance(Context context, ModelInterpretationContext mic) { 050 return new ConfigurationModelHandlerFull(context); 051 } 052 053 @Override 054 public void postHandle(ModelInterpretationContext mic, Model model) throws ModelHandlerException { 055 ConfigurationModel configurationModel = (ConfigurationModel) model; 056 057 // scanning is disabled 058 if (scanning != Boolean.TRUE) { 059 return; 060 } 061 062 String scanPeriodStr = mic.subst(configurationModel.getScanPeriodStr()); 063 scheduleReconfigureOnChangeTask(scanPeriodStr); 064 065 ConfigurationWatchList cwl = ConfigurationWatchListUtil.getConfigurationWatchList(getContext()); 066 if (cwl != null) { 067 try { 068 addInfo("Main configuration file URL: " + cwl.getTopURL()); 069 addInfo("FileWatchList= {" + cwl.getFileWatchListAsStr() + "}"); 070 addInfo("URLWatchList= {" + cwl.getUrlWatchListAsStr() + "}"); 071 } catch (NoSuchMethodError e) { 072 addWarn("It looks like the version of logback-classic is more recent than"); 073 addWarn("the version of logback-core. Please align the two versions."); 074 } 075 } 076 } 077 078 079 /** 080 * This method is called from logback-tyler version 1.0.4 and earlier. 081 * <p> 082 * This method assumes that the variables scanStr and scanPeriodStr have undergone variable substitution 083 * as applicable to their current environment 084 * 085 * @param scanPeriodStr 086 * @since 1.5.0 087 */ 088 public void detachedPostProcess(String scanStr, String scanPeriodStr) { 089 if (OptionHelper.isNullOrEmptyOrAllSpaces(scanStr) || CoreConstants.FALSE_STR.equalsIgnoreCase(scanStr)) { 090 return; 091 } 092 093 scheduleReconfigureOnChangeTask(scanPeriodStr); 094 } 095 096 /** 097 * This method is called from logback-tyler version 1.0.x and later. 098 * <p> 099 * This method assumes that the variables scanPeriodStr has undergone variable substitution 100 * as applicable to their current environment 101 * 102 * @param scanPeriodStr 103 * @since 1.5.28 104 */ 105 public void detachedPostProcess(String scanPeriodStr) { 106 scheduleReconfigureOnChangeTask(scanPeriodStr); 107 } 108 109 110 /** 111 * Schedules a task to monitor configuration changes and reconfigure the application when changes are detected. 112 * The task periodically scans for changes based on the given scan period and reconfigures the system if needed. 113 * This method ensures a valid configuration watch is in place before scheduling the task. 114 * 115 * @param scanPeriodStr the string value representing the scanning period, which will determine how often 116 * the task checks for configuration changes. The value is expected to have undergone 117 * necessary variable substitution for the current environment. 118 */ 119 private void scheduleReconfigureOnChangeTask(String scanPeriodStr) { 120 121 boolean watchPredicateFulfilled = ConfigurationWatchListUtil.watchPredicateFulfilled(context); 122 if (!watchPredicateFulfilled) { 123 addWarn(FAILED_WATCH_PREDICATE_MESSAGE_1); 124 addWarn(FAILED_WATCH_PREDICATE_MESSAGE_2); 125 return; 126 } 127 128 ReconfigureOnChangeTask rocTask = new ReconfigureOnChangeTask(); 129 rocTask.setContext(context); 130 131 addInfo("Registering a new ReconfigureOnChangeTask " + rocTask); 132 133 context.fireConfigurationEvent(ConfigurationEvent.newConfigurationChangeDetectorRegisteredEvent(rocTask)); 134 135 Duration duration = getDurationOfScanPeriodAttribute(scanPeriodStr, SCAN_PERIOD_DEFAULT); 136 137 ConfigurationWatchList cwl = ConfigurationWatchListUtil.getConfigurationWatchList(context); 138 139 String fileWatchListAsStr = (cwl != null) ? cwl.getFileWatchListAsStr() : ""; 140 141 addInfo("Will scan for changes in [" + fileWatchListAsStr + "] "); 142 // Given that included files are encountered at a later phase, the complete list 143 // of files to scan can only be determined when the configuration is loaded in full. 144 // However, scan can be active if mainURL is set. Otherwise, when changes are 145 // detected the top level config file cannot be accessed. 146 addInfo("Setting ReconfigureOnChangeTask scanning period to " + duration); 147 148 ScheduledExecutorService scheduledExecutorService = context.getScheduledExecutorService(); 149 ScheduledFuture<?> scheduledFuture = scheduledExecutorService.scheduleAtFixedRate(rocTask, duration.getMilliseconds(), duration.getMilliseconds(), 150 TimeUnit.MILLISECONDS); 151 rocTask.setScheduledFuture(scheduledFuture); 152 context.addScheduledFuture(scheduledFuture); 153 154 } 155 156 private Duration getDurationOfScanPeriodAttribute(String scanPeriodAttrib, Duration defaultDuration) { 157 Duration duration = null; 158 159 if (!OptionHelper.isNullOrEmptyOrAllSpaces(scanPeriodAttrib)) { 160 try { 161 duration = Duration.valueOf(scanPeriodAttrib); 162 } catch (IllegalStateException | IllegalArgumentException e) { 163 addWarn("Failed to parse 'scanPeriod' attribute [" + scanPeriodAttrib + "]", e); 164 // default duration will be set below 165 } 166 } 167 168 if (duration == null) { 169 addInfo("No 'scanPeriod' specified. Defaulting to " + defaultDuration.toString()); 170 duration = defaultDuration; 171 } 172 return duration; 173 } 174}