You are viewing a plain text version of this content. The canonical link for it is here.
Posted to cvs@cocoon.apache.org by re...@apache.org on 2009/06/01 13:27:52 UTC

svn commit: r780620 - in /cocoon/cocoon3/trunk: cocoon-docs/src/docbkx/reference/ cocoon-monitoring/src/main/java/org/apache/cocoon/monitoring/ cocoon-sample/rcl-config/WEB-INF/

Author: reinhard
Date: Mon Jun  1 11:27:51 2009
New Revision: 780620

URL: http://svn.apache.org/viewvc?rev=780620&view=rev
Log:
COCOON3-33
Support loading new configuration files via JMX operations
Documentation

Modified:
    cocoon/cocoon3/trunk/cocoon-docs/src/docbkx/reference/web-applications.xml
    cocoon/cocoon3/trunk/cocoon-monitoring/src/main/java/org/apache/cocoon/monitoring/Log4JReconfigurator.java
    cocoon/cocoon3/trunk/cocoon-monitoring/src/main/java/org/apache/cocoon/monitoring/LoggingConfigurationResetter.java
    cocoon/cocoon3/trunk/cocoon-sample/rcl-config/WEB-INF/log4j.xml

Modified: cocoon/cocoon3/trunk/cocoon-docs/src/docbkx/reference/web-applications.xml
URL: http://svn.apache.org/viewvc/cocoon/cocoon3/trunk/cocoon-docs/src/docbkx/reference/web-applications.xml?rev=780620&r1=780619&r2=780620&view=diff
==============================================================================
--- cocoon/cocoon3/trunk/cocoon-docs/src/docbkx/reference/web-applications.xml (original)
+++ cocoon/cocoon3/trunk/cocoon-docs/src/docbkx/reference/web-applications.xml Mon Jun  1 11:27:51 2009
@@ -309,6 +309,79 @@
     <title>Testing</title>
     <para>TBW: Integeration tests</para>
   </section>
+
+  <section id="webapps.monitoring">
+    <title>Monitoring support</title>
+    <para>
+      This module gives you the possibility of monitoring Cocoon 3 applications. It expose a simple API
+      via <literal>Spring JMX MBeans</literal> and it can be user via <literal>jconsole</literal>
+      or other <literal>JMX</literal> console.
+    </para>
+    <section id="webapps.monitoring.configuration">
+      <title>Module configuration</title>
+      <para>
+        All module configuration is defined in <literal>META-INF/cocoon/spring/cocoon-monitoring.xml.</literal>
+        Each part of module can be enabled or disabled by commenting (or removing) appropriate
+        entry in configuration file, by default all parts are enabled.    
+      </para>
+    </section>
+    <section id="webapps.monitoring.parts">
+      <title>Available parts in module</title>
+      <section id="webapps.monitoring.reconfiguration">
+        <title>Inspect logging settings and reconfigure them</title>
+        <section>
+          <title>Inspect all configured loggers</title>
+          <para>
+            The operation <code>getLoggers()</code> returns a list of all configured Log4j loggers in the 
+            application, it consists of pairs: <literal>&lt;class or package name&gt;</literal> (category)
+            => <literal>&lt;logging level&gt;</literal>
+          </para>
+        </section>
+        <section>
+          <title>Permanent change of a logging level</title>
+          <para>
+            For doing permanent changes of logging level for particular class or package use the operation
+            <code>setLoggingLevel(String category, String newLogLevel)</code>. Where 
+            <literal>Category</literal> is name of <literal>class</literal> or <literal>
+            package</literal> with you want to change logging level and <literal>newLogLevel
+            </literal> is one of logging level: <literal>OFF</literal>, <literal>INFO</literal>,
+            <literal>WARN</literal>, <literal>ERROR</literal>, <literal>FATAL</literal>,
+            <literal>TRACE</literal>, <literal>DEBUG</literal>, <literal>ALL</literal> (this parameter
+             isn't case sensitive, so you can also use lower case names).
+          </para>
+        </section>
+        <section>
+          <title>Temporal change of a logging level</title>
+          <para>
+            For doing temporal changes of logging level for particular class or package use the operation
+            <code>setLoggingTempoporalLevel(String category, String temporalLogLevel, String
+            timeOut)</code>. First two parameters are same as in <code>setLoggingLevel()</code>,
+            last one determinate how long this logging level should be used (after that amount of
+            time logging level would be set back to old level). <literal>timeOut</literal> parameter
+            should match regular expression: <literal>^[0-9.]+[dhm]?$</literal>, for example if value
+            of <literal>timeOut</literal> is set for <code>1,5h</code> that means that new logging level
+            would be active for one and half hour.
+          </para>
+        </section>
+        <section>
+          <title>Load a new configuration file</title>
+          <para>
+            For loading a completely new configuration file use the operation <code>loadNewConfigurationFile(String path)</code>.
+            This method is capable for both <literal>XML</literal> and <literal>properties</literal> configuration
+            files. There is only one parameter <literal>path</literal> that should contain absolute path to new
+            configuration file located locally on the server. Before performning any action that file is validated. First of
+            all the file extension (there are only two appropriate extensions: <literal>*.xml</literal>
+            and <literal>*.properties</literal>) is checked. The next validation step is different for both files, for 
+            <literal>XML</literal> files its content is validated against the Log4j <literal>DTD</literal> or
+            <literal>schema</literal> then all output log files are checked that they exist and they are writable.
+            <literal>Properties</literal> configuration files are validated that they contain at least one
+            appender and each output file directory exists and is writable. These validations are done to
+            prevent Log4j to get into an inconsistent state. 
+          </para>
+        </section>
+      </section>
+    </section>
+  </section>
   
   <section id="webapps.tutorial">
     <title>Tutorial</title>

Modified: cocoon/cocoon3/trunk/cocoon-monitoring/src/main/java/org/apache/cocoon/monitoring/Log4JReconfigurator.java
URL: http://svn.apache.org/viewvc/cocoon/cocoon3/trunk/cocoon-monitoring/src/main/java/org/apache/cocoon/monitoring/Log4JReconfigurator.java?rev=780620&r1=780619&r2=780620&view=diff
==============================================================================
--- cocoon/cocoon3/trunk/cocoon-monitoring/src/main/java/org/apache/cocoon/monitoring/Log4JReconfigurator.java (original)
+++ cocoon/cocoon3/trunk/cocoon-monitoring/src/main/java/org/apache/cocoon/monitoring/Log4JReconfigurator.java Mon Jun  1 11:27:51 2009
@@ -16,31 +16,78 @@
  */
 package org.apache.cocoon.monitoring;
 
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Enumeration;
 import java.util.List;
+import java.util.Scanner;
 
+import javax.naming.ConfigurationException;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.apache.cocoon.configuration.PropertyHelper;
+import org.apache.cocoon.configuration.Settings;
+import org.apache.commons.io.FilenameUtils;
 import org.apache.log4j.Level;
 import org.apache.log4j.LogManager;
 import org.apache.log4j.Logger;
+import org.apache.log4j.PropertyConfigurator;
 import org.apache.log4j.spi.LoggerRepository;
+import org.apache.log4j.xml.DOMConfigurator;
+import org.apache.log4j.xml.Log4jEntityResolver;
 import org.springframework.jmx.export.annotation.ManagedAttribute;
 import org.springframework.jmx.export.annotation.ManagedOperation;
 import org.springframework.jmx.export.annotation.ManagedOperationParameter;
 import org.springframework.jmx.export.annotation.ManagedOperationParameters;
 import org.springframework.jmx.export.annotation.ManagedResource;
+import org.w3c.dom.Document;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.helpers.DefaultHandler;
 
+/**
+ * This is a JMX MBean class that expose methods for log4j configuration.
+ */
 @ManagedResource
 public class Log4JReconfigurator {
 
-    private LoggerRepository loggerRepository;
+    private static final String[] EXTENSIONS = new String[] { "xml", "properties" };
+
+    private final Logger logger;
+    private final LoggerRepository loggerRepository;
+    private final DocumentBuilder docBuilder;
+
+    private Settings settings;
 
     public Log4JReconfigurator() {
         this.loggerRepository = LogManager.getLoggerRepository();
+        this.logger = Logger.getLogger(Log4JReconfigurator.class);
+
+        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+        dbf.setValidating(true);
+
+        try {
+            this.docBuilder = dbf.newDocumentBuilder();
+        } catch (ParserConfigurationException e) {
+            this.logger.fatal(e.getMessage(), e);
+            throw new RuntimeException(e);
+        }
+        this.docBuilder.setErrorHandler(new DefaultHandler());
+        this.docBuilder.setEntityResolver(new Log4jEntityResolver());
     }
 
+    /**
+     * Find all configured loggers and returns is as a array of <code>String</code>s
+     *
+     * @return list of all configured loggers with their level.
+     */
     @ManagedAttribute(description = "Return a list of all configured loggers with their level.")
-    public String[] getLoggers() {
+    public final String[] getLoggers() {
         List<String> result = new ArrayList<String>();
 
         @SuppressWarnings("unchecked")
@@ -55,11 +102,24 @@
         return result.toArray(new String[] {});
     }
 
-    @ManagedOperation(description = "Sets logging level for a paticular package or a class. Returns true if operation was succesful.")
+    /**
+     * Sets logging level for a particular package or a class.
+     *
+     * @param category name of the log category (usually a package or class name) whose log level
+     *            should be changed.
+     * @param newLogLevel new log level for that category. Available log levels are:
+     *            <code>OFF</code>, <code>INFO</code>, <code>WARN</code>, <code>ERROR</code>,
+     *            <code>FATAL</code>, <code>TRACE</code>, <code>DEBUG</code>, <code>ALL</code>
+     * @return <code>true</code> if operation was successful, <code>false</code> otherwise.
+     */
+    @ManagedOperation(description = "Sets logging level for a particular package or a class. Returns "
+            + "true if operation was successful.")
     @ManagedOperationParameters(value = {
-            @ManagedOperationParameter(name = "category", description = "Name of the log category (usually a package or class name) whose log level should be changed."),
-            @ManagedOperationParameter(name = "newLevel", description = "New log level for that category. Avaliable log levels are: OFF, INFO, WARN, ERROR, FATAL, TRACE, DEBUG, ALL") })
-    public boolean setLoggingLevel(String category, String newLogLevel) {
+            @ManagedOperationParameter(name = "category", description = "Name of the log category (usually "
+                    + "a package or class name) whose log level should be changed."),
+            @ManagedOperationParameter(name = "newLevel", description = "New log level for that category. "
+                    + "Available log levels are: OFF, INFO, WARN, ERROR, FATAL, TRACE, DEBUG, ALL") })
+    public final boolean setLoggingLevel(final String category, final String newLogLevel) {
         boolean result = false;
 
         Logger logger = this.loggerRepository.getLogger(category);
@@ -71,14 +131,32 @@
         return result;
     }
 
-    @ManagedOperation(description = "Sets new logging level for amout of time. After timeout log level is set back to old value.")
+    /**
+     * Sets new logging level for amount of time. After timeout log level is set back to old value.
+     *
+     * @param category name of the log category (usually a package or class name) whose log level
+     *            should be changed.
+     * @param temporalLogLevel temporal log level for that category that should be set for specified
+     *            amount of time.
+     * @param timeOut amount of time that temporalLevel should be active. Value of timeOut should
+     *            match regular expression: ^[0-9.]+[dhm]?$ where <code>d</code> means day,
+     *            <code>h</code> hours and <code>m</code> minutes
+     * @return <code>true</code> if operation was successful, <code>false</code> otherwise.
+     */
+    @ManagedOperation(description = "Sets new logging level for amount of time. After timeout log level is set"
+            + " back to old value.")
     @ManagedOperationParameters(value = {
-            @ManagedOperationParameter(name = "category", description = "Name of the log category (usually a package or class name) whose log level should be changed."),
-            @ManagedOperationParameter(name = "temporalLevel", description = "Temporal log level for that category that should be set for specified amout of time."),
-            @ManagedOperationParameter(name = "timeOut", description = "Amount of time that temporalLevel should be active. Value of timeOut should match regex: ^[0-9.]+[dhm]?$ where 'd' means day, 'h' hours and 'm' minutes") })
-    public boolean setLoggingTempoporalLevel(String category, String temporalLogLevel, String timeOut) {
+            @ManagedOperationParameter(name = "category", description = "Name of the log category (usually"
+                    + " a package or class name) whose log level should be changed."),
+            @ManagedOperationParameter(name = "temporalLevel", description = "Temporal log level for that "
+                    + "category that should be set for specified amount of time."),
+            @ManagedOperationParameter(name = "timeOut", description = "Amount of time that temporalLevel "
+                    + "should be active. Value of timeOut should match regular expression: ^[0-9.]+[dhm]?$ "
+                    + "where 'd' means day, 'h' hours and 'm' minutes") })
+    public final boolean setLoggingTempoporalLevel(final String category, final String temporalLogLevel,
+            final String timeOut) {
         if (!timeOut.matches("^[0-9.]+[dhm]?$")) {
-            throw new UnsupportedOperationException("Unsupported time out format: " + timeOut);
+            throw new UnsupportedOperationException("Unsupported time-out format: " + timeOut);
         }
 
         boolean result = false;
@@ -94,4 +172,178 @@
 
         return result;
     }
+
+    /**
+     * Allows to change configuration of log4j on the fly. This function support both XML and
+     * properties configuration files. Before reloading the configuration it checks that the new
+     * config file contains at least one appender and all output files are accessible (for XML
+     * configs it also validate XML syntax using schema or DTD)
+     *
+     * @param path absolute path to configuration file located on the server.
+     * @return <code>true</code> if operation was successful, <code>false</code> otherwise.
+     * @throws Exception if something unusual happens
+     */
+    @ManagedOperation(description = "Allows to change configuration of log4j on the fly. This function support "
+            + "both XML and properties configuration files. Before reloading configuration it checks that the new "
+            + "config file contains at least one appender and all output files are accessible (for XML configs it"
+            + "also validate XML syntax using schema or DTD)")
+    @ManagedOperationParameters(value = { @ManagedOperationParameter(name = "configFilePath", description = "Absolute path to configuration file.") })
+    public final boolean loadNewConfigurationFile(String path) throws Exception {
+        path = path.trim();
+
+        // check extension
+        if (FilenameUtils.isExtension(path, EXTENSIONS)) {
+            File newConfig = new File(path);
+
+            if (!newConfig.exists()) {
+                this.logger.fatal("Cannot find file: " + path);
+                throw new FileNotFoundException("Cannot find file: " + path);
+            } else if (!newConfig.canRead()) {
+                this.logAndThrowIOException("Cannot read file: " + path);
+            }
+
+            if (FilenameUtils.isExtension(path, "xml")) {
+                this.loadNewXMLConfigurationFile(path);
+            } else {
+                this.loadNewPropertiesConfigurationFile(path);
+            }
+        } else {
+            String message = "Unsupported file format: " + FilenameUtils.getExtension(path);
+            this.logger.fatal(message);
+            throw new ConfigurationException(message);
+        }
+
+        return true;
+    }
+
+    /**
+     * Inject the settings object.
+     *
+     * @param s The settings bean.
+     */
+    public final void setSettings(final Settings s) {
+        this.settings = s;
+    }
+
+    /**
+     * Loads XML config file specified by <code>path</code> parameter. Then this file is validate
+     * against DTD or schema, after that every appender is checked that it output file is
+     * accessible.
+     *
+     * @param path absolute path to XML configuration file.
+     * @throws Exception if something unusual happens
+     */
+    private void loadNewXMLConfigurationFile(final String path) throws Exception {
+        Document doc;
+        try { // validate XML config file
+            doc = this.docBuilder.parse(path);
+        } catch (Exception e) {
+            this.logger.fatal(e.getMessage(), e);
+            throw new IOException("Config file parse exception: " + e.getMessage());
+        }
+
+        // check access for all log files
+        NodeList appenderList = doc.getElementsByTagName("appender");
+        for (int i = 0; i < appenderList.getLength(); i++) {
+            NodeList childNodes = appenderList.item(i).getChildNodes();
+            for (int j = 0; j < childNodes.getLength(); j++) {
+                Node item = childNodes.item(j);
+                this.extractLogFilespathAndValidate(item);
+            }
+        }
+
+        // apply new settings
+        DOMConfigurator.configure(path);
+    }
+
+    /**
+     * Extracts path to log file from XML configuration node and checks that it exist and is
+     * writable.
+     *
+     * @param item
+     * @throws IOException if directory doesn't exist or isn't writable
+     */
+    private void extractLogFilespathAndValidate(final Node item) throws IOException {
+        if (!item.getNodeName().equalsIgnoreCase("param")) {
+            return;
+        }
+
+        NamedNodeMap itemAttributes = item.getAttributes();
+        if (itemAttributes == null) {
+            return;
+        }
+
+        Node paramName = itemAttributes.getNamedItem("name");
+        if (paramName != null && paramName.getNodeValue().equalsIgnoreCase("file")) {
+            Node paramValue = itemAttributes.getNamedItem("value");
+            if (paramValue != null) {
+                String logpath = paramValue.getNodeValue();
+                this.validateLogFile(logpath);
+            }
+        }
+    }
+
+    /**
+     * Loads properties config file specified by <code>path</code> parameter. Then it validate that
+     * file contains configuration for at least one appender and validate all configured log files
+     * that they path exist and it is writable.
+     *
+     * @param path absolute path to properties configuration file.
+     * @throws Exception if something unusual happens
+     */
+    private void loadNewPropertiesConfigurationFile(final String path) throws ConfigurationException, IOException {
+        boolean result = false;
+        File file = new File(path);
+
+        // search for appender configuration
+        Scanner fileScanner = new Scanner(file);
+        while (fileScanner.hasNext()) {
+            String line = fileScanner.nextLine();
+            if (!result && line.toLowerCase().matches("^log4j\\.appender\\.[\\w\\.]+=[\\w\\.]+$")) {
+                PropertyConfigurator.configure(file.getPath());
+                result = true; // we got our "at least one appender" ;)
+            }
+
+            if (line.toLowerCase().matches("^log4j\\.appender\\.[\\w]+\\.file=[\\w\\.\\{\\}\\$/\\:]+$")) {
+                String[] logFile = line.split("=");
+                this.validateLogFile(logFile[1]);
+            }
+        }
+
+        if (!result) {
+            throw new ConfigurationException(
+                    "No configured appenders, there should be at least one appender confgured.");
+        }
+    }
+
+    /**
+     * Helper method that logs exception and throws IOException.
+     *
+     * @param message that should be logged and passed to exception
+     * @throws IOException always throws this exception
+     */
+    private void logAndThrowIOException(final String message) throws IOException {
+        this.logger.fatal(message);
+        throw new IOException(message);
+    }
+
+    /**
+     * Validate log file path that it exists and is writable.
+     *
+     * @param logPath path to log file
+     * @throws IOException if log file directory doesn't exist or is read only
+     */
+    private void validateLogFile(String logPath) throws IOException {
+        String logDirpath = FilenameUtils.getFullPath(PropertyHelper.replace(logPath, this.settings));
+
+        if (logDirpath.length() == 0) { // if logDirpath is empty
+            logDirpath = "."; // current directory is log directory
+        }
+
+        if (!new File(logDirpath).exists()) {
+            this.logAndThrowIOException("Log directory: " + logDirpath + " does not exist.");
+        } else if (!new File(logPath).canWrite()) {
+            this.logAndThrowIOException("Log file: " + logPath + " is read only.");
+        }
+    }
 }

Modified: cocoon/cocoon3/trunk/cocoon-monitoring/src/main/java/org/apache/cocoon/monitoring/LoggingConfigurationResetter.java
URL: http://svn.apache.org/viewvc/cocoon/cocoon3/trunk/cocoon-monitoring/src/main/java/org/apache/cocoon/monitoring/LoggingConfigurationResetter.java?rev=780620&r1=780619&r2=780620&view=diff
==============================================================================
--- cocoon/cocoon3/trunk/cocoon-monitoring/src/main/java/org/apache/cocoon/monitoring/LoggingConfigurationResetter.java (original)
+++ cocoon/cocoon3/trunk/cocoon-monitoring/src/main/java/org/apache/cocoon/monitoring/LoggingConfigurationResetter.java Mon Jun  1 11:27:51 2009
@@ -22,13 +22,24 @@
 import org.apache.log4j.Level;
 import org.apache.log4j.Logger;
 
+/**
+ * This class starts a new thread that restores the logging level for a particular class or package
+ * (after a defined amount of time).
+ */
 public class LoggingConfigurationResetter {
 
-    private long delay;
-    private Logger logger;
-    private Level oldLogLevel;
-
-    public LoggingConfigurationResetter(final Logger logger, final Level oldLevel, String timeout) {
+    private final long delay;
+    private final Logger logger;
+    private final Level oldLogLevel;
+
+    /**
+     * @param logger instance of logger that logging level should changed
+     * @param oldLevel logging level value that should be set after timeout
+     * @param timeout value of timeout. Is should match regular expression: ^[0-9.]+[dhm]?$
+     *          where <code>d</code> means day, <code>h</code> hours and <code>m</code>
+     *          minutes
+     */
+    public LoggingConfigurationResetter(final Logger logger, Level oldLevel, String timeout) {
 
         this.logger = logger;
         this.oldLogLevel = oldLevel;
@@ -36,6 +47,9 @@
         long factor;
         char unit = timeout.charAt(timeout.length() - 1); // get last char, it should be our unit
         switch (unit) {
+        case 's': // second
+            factor = 1 * 1000;
+            break;
         case 'm': // minute
             factor = 60 * 1000;
             break;
@@ -46,16 +60,20 @@
             factor = 24 * 60 * 60 * 1000;
             break;
         default:
-            throw new UnsupportedOperationException("Unsupporterd unit: " + unit);
+            String message = "Unsupported unit: " + unit;
+            throw new UnsupportedOperationException(message);
         }
 
         float multipler = Float.parseFloat(timeout.substring(0, timeout.length() - 1));
         this.delay = Math.round(multipler * factor);
-
     }
 
+    /**
+     * Starts thread
+     */
     public void start() {
         TimerTask task = new TimerTask() {
+
             @Override
             public void run() {
                 LoggingConfigurationResetter.this.logger.setLevel(LoggingConfigurationResetter.this.oldLogLevel);

Modified: cocoon/cocoon3/trunk/cocoon-sample/rcl-config/WEB-INF/log4j.xml
URL: http://svn.apache.org/viewvc/cocoon/cocoon3/trunk/cocoon-sample/rcl-config/WEB-INF/log4j.xml?rev=780620&r1=780619&r2=780620&view=diff
==============================================================================
--- cocoon/cocoon3/trunk/cocoon-sample/rcl-config/WEB-INF/log4j.xml (original)
+++ cocoon/cocoon3/trunk/cocoon-sample/rcl-config/WEB-INF/log4j.xml Mon Jun  1 11:27:51 2009
@@ -19,7 +19,6 @@
  -->
 <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
 <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
-
   <appender name="COCOON_DEFAULT" class="org.apache.log4j.FileAppender">
     <param name="File" value="./target/work/log/cocoon.log"/>
     <param name="Append" value="true"/>
@@ -36,5 +35,4 @@
     <priority value="WARN"/>
     <appender-ref ref="COCOON_DEFAULT"/>
   </root>
-
 </log4j:configuration>