Tuesday, July 17, 2012

ServletConfig for Log4J

In my previous post I had said that the ReverseEngineer functionality for Log4J had proven to be most useful in a servlet environment. Here is the servlet that makes it so. With this ConfigServlet, a web application can with a single page display the active Log4J configuration in a simple text box and allow the user to change that configuration and submit it back to the web application to change the Log4J configuration on the fly.

What this means is no more needing to set log levels to debug or trace at the beginning of the container resulting in huge amounts of log entries you don't need to see. Don't change the log settings until just before you test! Need to peek into a running system? Or you decide to examine just one category of logs? Or perhaps to suppress others? No need to shut down the web application and restart, just a simple load, alter and submit.

Of course, there is a significant security risk here, this page should not be visible to untrusted users. For my own usage, I was developing and maintaining a web application so this one servlet saved me a significant amount of time and effort by enabling me to be both quicker and more precise.

ConfigServlet is very primitive as servlets go, just spitting out a simple HTML form with two radio buttons, a text area, and a submit button. It could certainly be significantly beautified for today's frameworks. For this, though, style was irrelevant and functionality rules, so here it is in base form.

To set the configuration, the servlet takes query or post parameters for format (Property or XML) and config (the text content). Whether or not the configuration is set, both the property-style output and the DOM-style output for the current Log4J configuration are generated and available by selecting the desired radio button on the form. The same radio button determines which format is used to reconfigure Log4J if the submit button is selected. The radio button is the format parameter and the textarea contents are the config parameter back to the same servlet.

Below the ConfigServlet is a HTTPUnit 1.6-based JUnit 3.8 test that directly tests the ConfigServlet. Always write your tests!

package log4j.servlet;

import java.io.*;
import java.net.URL;
import java.net.MalformedURLException;
import java.util.*;
import java.util.regex.*;

import javax.servlet.ServletException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.http.*;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
import org.apache.log4j.xml.DOMConfigurator;

import org.xml.sax.SAXException;

import log4j.util.ReverseEngineer;

import org.apache.log4j.Logger;
import org.apache.log4j.LogManager;
import org.apache.log4j.helpers.OptionConverter;

/**
   Provided as a HTML fragment.
 */
public class ConfigServlet extends HttpServlet
{
    protected void doPost (HttpServletRequest request, HttpServletResponse response) throws IOException
    {
        doGet(request, response);
    }

    protected void doGet (HttpServletRequest request, HttpServletResponse response) throws IOException
    {
        process(request, response);
    }

    public static void process (HttpServletRequest request, HttpServletResponse response) throws IOException
    {
        configure(request.getParameter("format"), request.getParameter("config"), response.getWriter());
        generate(request.getRequestURI(), response.getWriter());
    }

    private static void configure (String format, String config, PrintWriter out) throws IOException
    {
        if (format != null && config != null)
        {
            if (format.equals("Property"))
            {
                Properties propertyConfig = new Properties();
                propertyConfig.load(new ByteArrayInputStream(config.getBytes()));
                PropertyConfigurator.configure(propertyConfig);
                out.print("<p class=\"log4jConfig\">Logging configured!</p>");
            }
            else if (format.equals("XML"))
            {
                try
                {
                    new DOMConfigurator().doConfigure(new ByteArrayInputStream(config.getBytes()), LogManager.getLoggerRepository());
                    out.print("<p class=\"log4jConfig\">Logging configured!</p>");
                }
                catch (FactoryConfigurationError ex)
                {
                    out.print("<p class=\"log4jConfig.error\">XML parsing failed: ");
                    out.print(ex.getMessage());
                    out.print("</p>");
                }
            }
            else
            {
                out.print("<p class=\"log4jConfig.error\">Unknown format: ");
                out.print(format);
                out.print("</p>");
            }
        }
    }

    private static void generate (String contextPath, PrintWriter out) throws IOException
    {
        StringWriter sw = new StringWriter();
        ReverseEngineer.writeDOMConfiguration(LogManager.getLoggerRepository(), sw);
        String dom = sw.toString();
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        ReverseEngineer.writePropertyConfiguration(LogManager.getLoggerRepository(), bout);
        String properties = new String(bout.toByteArray());

        String newLine = System.getProperty("line.separator");

        out.print("<form ID=\"log4jConfig\" name=\"log4jConfig\" method=\"get\" action=\"");
        out.print(contextPath);
        out.print("\">");
        out.print(newLine);
        out.print(newLine);

        out.print("<p>");
        out.print(newLine);
        out.print("<span class=\"log4jConfig\">Format:</span>");
        out.print(newLine);
        out.print("<input ID=\"log4jConfig.propertyFormat\" type=\"radio\" name=\"format\" value=\"Property\" checked onclick=\"document.log4jConfig.config.value = '");
        out.print(Pattern.compile("\r?\n").matcher(ReverseEngineer.escapeXml(properties)).replaceAll("\\\\n"));
        out.print("';\"/> <span class=\"log4jConfig\">Properties</span>  ");
        out.print(newLine);
        out.print("<input ID=\"log4jConfig.xmlFormat\" type=\"radio\" name=\"format\" value=\"XML\" onclick=\"document.log4jConfig.config.value = '");
        out.print(Pattern.compile("\r?\n").matcher(ReverseEngineer.escapeXml(dom)).replaceAll("\\\\n"));
        out.print("'\"/> <span class=\"log4jConfig\">XML</span>  ");
        out.print(newLine);
        out.print("</p>");
        out.print(newLine);
        out.print(newLine);

        out.print("<p>");
        out.print(newLine);
        out.print("<span class=\"log4jConfig\">Configuration</span>");
        out.print(newLine);
        out.print("</p>");
        out.print(newLine);
        out.print(newLine);

        out.print("<textarea ID=\"log4jConfig.config\" name=\"config\" cols=\"100\" rows=\"25\" wrap=\"off\">");
        out.print(properties);
        out.print("</textarea>");

        out.print("<p>");
        out.print(newLine);
        out.print("<input ID=\"log4jConfig.submit\" type=\"submit\" value=\"Configure\"/>");
        out.print(newLine);
        out.print("<input ID=\"log4jConfig.reset\" type=\"reset\" value=\"Start Over\"/>");
        out.print(newLine);
        out.print("</p>");
        out.print(newLine);
        out.print(newLine);

        out.print("</form>");
        out.print(newLine);
        out.flush();
    }

    public void init (ServletConfig config) throws ServletException
    {
        ServletContext context = config.getServletContext();
        String log4jConfig = config.getInitParameter("log4j.configuration");
        String log4jConfiguratorClass = config.getInitParameter("log4j.configuratorClass");

        if (log4jConfig != null)
        {
            try
            {
                URL log4jConfigUrl = context.getResource(log4jConfig);
                if (log4jConfigUrl != null)
                {
                    OptionConverter.selectAndConfigure(log4jConfigUrl, log4jConfiguratorClass, LogManager.getLoggerRepository());
                    Logger.getLogger(getClass()).info("Log4J configured from " + log4jConfig + " using " + log4jConfiguratorClass);
                }
            }
            catch (MalformedURLException badUrl)
            {
                Logger.getLogger(getClass()).warn("Could not load Log4J configuration from " + log4jConfig + " using " + log4jConfiguratorClass, badUrl);
            }
        }
    }
}


package unit.logging;

import java.io.*;
import java.util.*;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.log4j.*;
import org.apache.log4j.spi.*;
import org.apache.log4j.xml.*;

import junit.framework.*;
import com.meterware.httpunit.*;
import com.meterware.servletunit.*;

import log4j.util.*;

public class ConfigServletTest extends TestCase
{
    public static final Logger LOGGER = Logger.getLogger(ConfigServletTest.class);

    public ConfigServletTest (String name)
    {
        super(name);
    }

    public void testPropertyConfig () throws Exception
    {
        ServletRunner servletRunner = new ServletRunner();
        servletRunner.registerServlet("/config", "log4j.servlet.ConfigServlet");
        ServletUnitClient client = servletRunner.newClient();
        WebRequest request = new PostMethodWebRequest("http://localhost/config");
        WebResponse response = client.getResponse(request);
        String config = response.getFormWithID("log4jConfig").getParameterValue("config");
        Properties propertiesConfig = new Properties();
        propertiesConfig.load(new ByteArrayInputStream(config.getBytes()));

        LoggerRepository repository = LogManager.getLoggerRepository();
        Properties propertiesConfig2 = ReverseEngineer.getPropertyConfiguration(repository);

        assertEquals("Property configuration did not match", propertiesConfig2, propertiesConfig);

        repository.resetConfiguration();
        request = new PostMethodWebRequest("http://localhost/config");
        response = client.getResponse(request);
        Properties resetConfig = new Properties();
        resetConfig.load(new ByteArrayInputStream(response.getFormWithID("log4jConfig").getParameterValue("config").getBytes()));
        assertFalse("Property configuration did not reset", propertiesConfig2.equals(resetConfig));
        assertEquals("Configuration failed to match after reset", ReverseEngineer.getPropertyConfiguration(repository), resetConfig);

        WebForm configForm = response.getFormWithID("log4jConfig");
        configForm.setParameter("config", config);
        response = configForm.submit();
        Properties propertiesConfig3 = new Properties();
        propertiesConfig3.load(new ByteArrayInputStream(response.getFormWithID("log4jConfig").getParameterValue("config").getBytes()));

        assertEquals("Restored property configuration did not match", propertiesConfig2, propertiesConfig3);
    }

    public void testDOMConfig () throws Exception
    {
        LoggerRepository repository = LogManager.getLoggerRepository();

        ServletRunner servletRunner = new ServletRunner();
        servletRunner.registerServlet("/config", "log4j.servlet.ConfigServlet");
        ServletUnitClient client = servletRunner.newClient();
        WebRequest request = new PostMethodWebRequest("http://localhost/config");
        WebResponse response = client.getResponse(request);
        WebForm configForm = response.getFormWithID("log4jConfig");
        configForm.setParameter("format", "XML");

        String config = Pattern.compile("\r?\n").matcher(configForm.getParameterValue("config")).replaceAll(System.getProperty("line.separator"));

        StringWriter sw = new StringWriter();
        ReverseEngineer.writeDOMConfiguration(repository, sw);
        String config2 = Pattern.compile("\r?\n").matcher(sw.toString()).replaceAll(System.getProperty("line.separator"));

        assertEquals("XML configurations did not match", config2, config);

        repository.resetConfiguration();
        request = new PostMethodWebRequest("http://localhost/config");
        response = client.getResponse(request);
        configForm = response.getFormWithID("log4jConfig");
        configForm.setParameter("format", "XML");
        String resetConfig = configForm.getParameterValue("config");

        assertFalse("XML configurations did not reset", resetConfig.equals(config));

        configForm.setParameter("config", config);
        response = configForm.submit();
        configForm = response.getFormWithID("log4jConfig");
        configForm.setParameter("format", "XML");
        String config3 = Pattern.compile("\r?\n").matcher(configForm.getParameterValue("config")).replaceAll(System.getProperty("line.separator"));

        assertEquals("Restored XML configuration did not match", config2, config3);
    }
}

No comments:

Post a Comment