Log4j Runtime Reloading

Einleitung
In diesem Blogeintrag werde ich zeigen, wie man die in einer J2EE Umgebung die Log4j Konfiguration von Servlet Container und deplolyten Webapps zur Laufzeit — also ohne Neustart oder Redeploy — neu einlesen kann.

Log4j bietet zu diesem Zweck bereits zwei Methoden an, die jeweils in den Klassen PropertyConfigurator bzw. DOMConfigurator zu finden sind. Diese sind für unsere Zwecke allerdings nicht geeignet (von der Benutzung wird vielerorts abgeraten), da sie einen eigenen Thread starten, welcher die log4j.properties bzw. log4j.xml auf Änderungen hin überwacht.
Das starten von Threads ist in J2EE Umgebungen jedoch verboten, da diese in der Regel nicht ordnungsgemäß beendet werden und zu Speicherlöchern führen.

JBoss und Tomcat7 erkennen Änderungen an ihrer eigenen Log4j Konfiguration bereits von Haus aus. Die Live Konfiguration von Webapps wird aber nicht unterstützt.
Aus diesem Grund und für Tomcat Versionen kleiner 7.0 bleibt die diese Lösung deshalb totzdem sinnvoll.

Servlet Container
Um die Konfiguration des Servlet Containers verändern und exakt wie in Log4j implementiert neu einzulesen
wurde ein Teil der Inititialisierungsroutinge aus Log4j 1.2.16 kopiert und in eine Hilfklasse gepackt. Diese führt
bei Aufruf der Methode reLoadConfig() einen Reset der Log4j Konfiguration durch und läd sie anschließend
neu, wie es das Log4j Packet selbst auch tun würde.

        /**
	 * @author Florian Loeffler <florian.loeffler@rrze.uni-erlangen.de>
	 */
	public static class Log4jUtil {
		private static final String DEFAULT_XML_CONFIGURATION_FILE = "log4j.xml";
		private static final String DEFAULT_CONFIGURATION_FILE = "log4j.properties";
		private static final String DEFAULT_INIT_OVERRIDE_KEY = "log4j.defaultInitOverride";
		private static final String DEFAULT_CONFIGURATION_KEY = "log4j.configuration";
		private static final String CONFIGURATOR_CLASS_KEY = "log4j.configuratorClass";
		public static final String UNKNOWN_SOURCE = "unknown";
		public static String configurationSource = UNKNOWN_SOURCE;

               /**
                * Reload Log4j configuration
                */
		public static void reLoadConfig() {
			LogManager.resetConfiguration();
			Log4jUtil.loadConfig();
		}

		/**
		 * Most of this was stolen from the {@link org.apache.log4j.LogManager}
		 * static initialization method.
		 */
		public static void loadConfig() {
			configurationSource = UNKNOWN_SOURCE;
			String override = OptionConverter.getSystemProperty(DEFAULT_INIT_OVERRIDE_KEY, null);

    		if (override == null || "false".equalsIgnoreCase(override)) {
				String configurationOptionStr = OptionConverter.getSystemProperty(DEFAULT_CONFIGURATION_KEY, null);
				String configuratorClassName = OptionConverter.getSystemProperty(CONFIGURATOR_CLASS_KEY, null);
				URL url = null;
				if (configurationOptionStr == null) {
					url = Loader.getResource(DEFAULT_XML_CONFIGURATION_FILE);
					if (url == null) {
						url = Loader.getResource(DEFAULT_CONFIGURATION_FILE);
					}
				} else {
					try {
						url = new URL(configurationOptionStr);
					} catch (MalformedURLException ex) {
						url = Loader.getResource(configurationOptionStr);
					}
				}

				if (url != null) {
					LogLog.debug("Using URL [" + url + "] for automatic log4j configuration.");
					configurationSource = url.getFile();
					try {
						OptionConverter.selectAndConfigure(url, configuratorClassName, LogManager.getLoggerRepository());
					} catch (NoClassDefFoundError e) {
						LogLog.warn("Error during default initialization", e);
					}
				} else {
					LogLog.debug("Could not find resource: [" + configurationOptionStr + "].");
				}
			} else {
				LogLog.debug("Default initialization of overridden by " + DEFAULT_INIT_OVERRIDE_KEY + "property.");
			}
		}
	}

Implementiert und getestet wurde diese Lösung mit dem Apache Tomcat 6.0.18.
Diese Lösung führt den Reload getreu der Log4j internen Implementierung durch und ist die „sauberste“ Lösung.
Für die Webapps wurde eine ewas kürzere Variante gewählt.

Webapps
Um die Log4j Konfiguration einer Webapp neu zu laden muss wegen der Classloader-Trennung in Tomcat etwas mehr Aufwand getrieben werden.

Konventionen
Es wird im Folgenden angenommen, dass die Webapp ihr eigenes log4j.jar im /lib Verzeichnis und ihre eigene log4j.properties bzw. log4j.xml im /classes Verzeichnis mitbringt.

Da die intern deployte Log4j Konfigurationsdatei der Webapp nicht verändert werden kann ohne einen Redeploy auszulösen wird bei einem Reload eine Konfigurationsdatei mit dem Namen log4j_{WebappDeploymentPath}.properties bzw. log4j_{WebappDeploymentPath}.xml aus dem Classpath geladen.

Beispiel
(zur Vereinfachung hier nur mit log4.properties)
Webapp test1 ist unter /test1 deployt und läd seine Log4j Konfiguration aus der mitdeploytem Log4j Konfigurationsdatei bei der Initialisierung.
Soll die Konfiguration nun zur Laufzeit durch eine andere ersetzt werden, so wird die deployte log4j.properties z.B. nach tomcat/lib/log4j_test1.properties kopiert und entsprechend angepasst. Beim Auslösen des Reload wird die aktuelle Konfiguration resettet und durch die in tomcat/lib/log4j_test1.properties ersetzt.

Implementierung
Für die Implementierung sind einige Dinge bezüglich Classloadern zu beachten. Siehe dazu auch das Apache Tomcat 6.0
Class Loader HOW-TO
und den Apache Tomcat 6.0.18 Source Code.

Die entsprechende Methode (hier schon zum Teil auf XCS aufbauend) sieht dann so aus:

	public String reloadWebappConfiguration(String contextPath) {
		String configFile = "log4j_" + contextPath.substring(1);

		Container hostContainer = XCSRegistry.getHostContainer("localhost");
		Container[] children = hostContainer.findChildren();
		for (Container c : children) {
			Context ctx = (Context) c;
			if (ctx.getPath().equals(contextPath)) {

				// Switch to webapp class loader
				ClassLoader loader = ctx.getManager().getContainer().getLoader().getClassLoader();
				ClassLoader oldLoader = Thread.currentThread().getContextClassLoader();
				Thread.currentThread().setContextClassLoader(loader);

				try {
					// locate configuration file
					URL configURL = null;
					String type = "xml";

					Class loaderClass = loader.loadClass("org.apache.log4j.helpers.Loader");
					Method m1 = loaderClass.getMethod("getResource", new Class[] { String.class });
					configURL = (URL) m1.invoke(null, configFile + ".xml");
					if (configURL == null) {
						type = "properties";
						configURL = (URL) m1.invoke(null, configFile + ".properties");
					}

					if (configURL == null) {
						break;
					}

					// reset configuration
					Class logManagerClass = loader.loadClass("org.apache.log4j.LogManager");
					logManagerClass.getMethod("resetConfiguration", null).invoke(null, null);

					// load properties file
					if (type.equals("xml")) {
						Class domConfiguratorClass = loader.loadClass("org.apache.log4j.xml.DOMConfigurator");
						Method m = domConfiguratorClass.getMethod("configure", new Class[] { URL.class });
						m.invoke(null, configURL);
					} else {
						Class propertyConfiguratorClass = loader.loadClass("org.apache.log4j.PropertyConfigurator");
						Method m = propertyConfiguratorClass.getMethod("configure", new Class[] { URL.class });
						m.invoke(null, configURL);
					}

				} catch (Exception e) {
				} finally {
					// reset classloader
					Thread.currentThread().setContextClassLoader(oldLoader);
				}
				break;
			}
		}
		return ret;
	}

Das Besondere ist das Log4j in der Methode configure() vor allem anderen die ContextClassLoader Property des aktuellen Threads nutzt, um die sich einen Classloader für das Laden der Konfigurationsdatei zu besorgen. Setzt man diese Property also nicht auf den Classloader, den man auch für das Laden der Log4j Klassen verwendet (in diesem Fall der WebappClassloader) wird man Probleme bekommen.

XCS Integration
Um die vorgestellten Codeteile aufzurufen könnte man sie zum Beispiel in ein eigenes Servlet o.ä. einbinden, das man dann vom Webbrowser aus aufruft oder eine Tomcat Erweiterung schreiben.

Hierfür bietet sich in das bereits bestehende Tomcat XCS Projekt an, da es einen integrierten RMI Server bietet, der einen Aufruf der (Tomcat-internen) Methoden von der Kommandozeile ermöglicht.

Ab XCS Version 1.7.1 sind die vorgestellten Methoden integriert und können genutzt werden:

$> java -cp tomcat-xcs.jar de.rrze.xcs.cli.Log4j -h
Usage: java -cp tomcat-xcs.jar de.rrze.xcs.cli.Log4j {WEBAPP_CONTEXT}
Reload log4j configuration of Tomcat and its' embedded webapps.

Example: java -cp tomcat-xcs.jar de.rrze.xcs.cli.Log4j /vv-web

Options
  WEBAPP_CONTEXT	Leave empty for Tomcat's log4j or set to the deployment path of a webapp.