Tuesday, July 31, 2012

Providing Map Context In Log4J Output

Although Log4J's PatternLayout provides a way to output specifically named values from the mapped diagnostic context (MDC), there is no mechanism in any of Log4J's layouts to simply display all of the values from the MDC. In fact, the LoggingEvent does not allow access to the MDC's map or even a copy of it.

This shortcoming was particularly annoying because at various points in my code I would want to add and remove arbitrary key/value pairs from the MDC at certain points specifically to make the information available in log files. This was why I decided to extend LoggingEvent to provide access to the set of keys in the MDC's map, thus allowing other classes aware of this functionality to access the full contents of the MDC and even to display it in layouts.

This implementation is in multiple classes provided below:
  1. OpenLoggingEvent which adds getMDCKeys() to the LoggingEvent.
  2. OpenLogger which extends Logger and changes the forcedLog() method to create an OpenLoggingEvent instead of a LoggingEvent.
  3. OpenLoggerFactory which is a LoggerFactory that will attempt to install itself as the default provider via LogManager.
  4. OpenLoggerConfigurator which can be specified by the log4j.configuratorClass system property in order for OpenLoggers to be used by default during Log4J during configuration.
  5. MDCAwareHTMLLayout which extends HTMLLayout with behavior to display the MDC map's keys and values in a HTML table.
  6. MDCAwareXMLLayout which extends XMLLayout with behavior to provide the MDC map's contents via <log4j:mdc> and <log4j:mdcEntry> tags.

 package log4j.util;

import java.util.Arrays;
import java.util.Collection;
import java.util.Hashtable;

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

public class OpenLoggingEvent extends LoggingEvent
{
    private transient Category logger;
    private String [] mdcKeys;

    public OpenLoggingEvent(String fqnOfCategoryClass, Category logger,
                            Priority level, Object message, Throwable throwable)
    {
        super(fqnOfCategoryClass, logger, level, message, throwable);
        this.logger = logger;
    }

    public OpenLoggingEvent(String fqnOfCategoryClass, Category logger,
                            long timeStamp, Priority level, Object message,
                            Throwable throwable)
    {
        super(fqnOfCategoryClass, logger, timeStamp, level, message, throwable);
        this.logger = logger;
    }


    // Should this be synchronized?  The superclass doesn't do it.
    public void getMDCCopy()
    {
        if (mdcKeys == null)
        {
            Hashtable mdc = MDC.getContext();
            if (mdc == null)
            {
                mdcKeys = new String [0];
            }
            else
            {
                mdcKeys = (String [])mdc.keySet().toArray(new String[mdc.size()]);
            }
        }
        super.getMDCCopy();
    }

    public Collection getMDCKeys ()
    {
        if (mdcKeys == null)
        {
            getMDCCopy();
        }
        return Arrays.asList(mdcKeys);
    }

    protected Category getLogger ()
    {
        return logger;
    }
}




package log4j.util;

import org.apache.log4j.Logger;
import org.apache.log4j.Priority;

class OpenLogger extends Logger
{
    protected OpenLogger (String name)
    {
        super(name);
    }

    protected void forcedLog (String fqcn, Priority level, Object message, Throwable t)
    {
        callAppenders(new OpenLoggingEvent(fqcn, this, level, message, t));
    }
}




package log4j.util;

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

public class OpenLoggerFactory implements LoggerFactory
{
    public static void install ()
    {
        try
        {
            LogManager.setRepositorySelector(new RepositorySelector()
                {
                    private Hierarchy repository = null;

                    {
                        OpenLoggerFactory factory = new OpenLoggerFactory();
                        Logger root = factory.makeNewLoggerInstance("root");
                        root.setLevel(LogManager.getLoggerRepository().getRootLogger().getLevel());
                        repository = new FactoryConfigurableHierarchy (factory, root);
                    }

                    public LoggerRepository getLoggerRepository ()
                    {
                        return repository;
                    }
                },
                                             null);  // no guard
        }
        catch (IllegalArgumentException ohWell)
        {
            // Guard prevented new repository
            Logger.getLogger(OpenLoggerFactory.class).info("Could not set RepositorySelector", ohWell);
        }
    }

    public Logger makeNewLoggerInstance (String name)
    {
        return new OpenLogger(name);
    }
}




package log4j.util;

import java.net.URL;

import org.apache.log4j.LogManager;
import org.apache.log4j.PropertyConfigurator;
import org.apache.log4j.helpers.OptionConverter;
import org.apache.log4j.spi.Configurator;
import org.apache.log4j.spi.LoggerRepository;
import org.apache.log4j.xml.DOMConfigurator;

/**
   Define system property log4j.configuratorClass to log4j.util.OpenLoggerConfigurator
   in order for the Log4J startup process to use OpenLoggers during configuration.
 */

public class OpenLoggerConfigurator implements Configurator
{
    static
    {
        OpenLoggerFactory.install();
    }

    public OpenLoggerConfigurator () {}

    public void doConfigure (URL url, LoggerRepository repository)
    {
        OptionConverter.selectAndConfigure(url, null, LogManager.getLoggerRepository());
    }
}



package log4j.util;

import java.util.*;

import org.apache.log4j.*;
import org.apache.log4j.or.RendererMap;
import org.apache.log4j.spi.*;
import org.apache.log4j.helpers.Transform;

public class MDCAwareHTMLLayout extends HTMLLayout
{
    public static final Logger LOGGER = Logger.getLogger(MDCAwareHTMLLayout.class);

    private RendererSupport rendererSupport;

    public MDCAwareHTMLLayout ()
    {
        super();
        if (LOGGER.getLoggerRepository() instanceof RendererSupport)
        {
            rendererSupport = (RendererSupport)LOGGER.getLoggerRepository();
        }
    }

    public String getRendererSupportClass ()
    {
        return (rendererSupport == null)
            ? null
            : rendererSupport.getClass().getName();
    }

    public void setRendererSupportClass (String className)
    {
        try
        {
            Class c = Class.forName(className, false, Thread.currentThread().getContextClassLoader());
            if (RendererSupport.class.isAssignableFrom(c))
            {
                rendererSupport = (RendererSupport)c.newInstance();
            }
            else
            {
                LOGGER.warn(className + " does not implement " + RendererSupport.class.getName());
            }
        }
        catch (ClassNotFoundException cnfe)
        {
            LOGGER.warn("Could not find class " + className, cnfe);
        }
        catch (IllegalAccessException illEx)
        {
            LOGGER.warn("Could not create instance of class " + className, illEx);
        }
        catch (InstantiationException isAbstract)
        {
            LOGGER.warn("Can not create instance of abstract class " + className, isAbstract);
        }
        catch (ExceptionInInitializerError uhOh)
        {
            LOGGER.warn("Could not initialize class " + className, uhOh);
        }
        catch (SecurityException secEx)
        {
            LOGGER.warn("Not allowed to create instance of class " + className, secEx);
        }
    }

    public String format (LoggingEvent event)
    {
        String formatted = super.format(event);
        if (event instanceof OpenLoggingEvent)
        {
            OpenLoggingEvent openEvent =(OpenLoggingEvent)event;
            Collection mdcKeys = new TreeSet(openEvent.getMDCKeys());
            if (mdcKeys.size() > 0)
            {
                StringBuffer buf = new StringBuffer(formatted);

                buf.append("<tr><td bgcolor=\"#EEEEEE\" class=\"MDC\" colspan=\"6\" title=\"Mapped Diagnostic Context\">")
                    .append(Layout.LINE_SEP)
                    .append("<strong>Mapped Diagnostic Context</strong>")
                    .append(Layout.LINE_SEP)
                    .append("<table border=\"1\">")
                    .append("<tr><th>Key</th>")
                    .append(Layout.LINE_SEP)
                    .append("<th>Value Class</th>")
                    .append(Layout.LINE_SEP)
                    .append("<th>Rendered Value</th></tr>")
                    .append(Layout.LINE_SEP);

                RendererMap rendererMap = null;

                if (openEvent.getLogger() != null)
                {
                    LoggerRepository maybeRendererSupport = openEvent.getLogger().getLoggerRepository();
                    if (maybeRendererSupport instanceof RendererSupport)
                    {
                        rendererMap = ((RendererSupport)maybeRendererSupport).getRendererMap();
                    }
                }
                if (rendererMap == null && rendererSupport != null)
                {
                    rendererMap = rendererSupport.getRendererMap();
                }

                for (Iterator keys = mdcKeys.iterator(); keys.hasNext();)
                {
                    String nextKey = (String)keys.next();
                    Object nextValue = event.getMDC(nextKey);
                    String val = (rendererMap == null)
                        ? String.valueOf(nextValue)
                        : rendererMap.findAndRender(nextValue);
                    buf.append("<tr><td>")
                        .append(Transform.escapeTags(nextKey))
                        .append("</td>")
                        .append(Layout.LINE_SEP)
                        .append("<td>")
                        .append(nextValue.getClass().getName())
                        .append("</td>")
                        .append(Layout.LINE_SEP)
                        .append("<td>")
                        .append(Transform.escapeTags(val))
                        .append("</td></tr>")
                        .append(Layout.LINE_SEP);
                }

                buf.append("</table></td></tr>").append(Layout.LINE_SEP);
                formatted = buf.toString();
            }
        }
        return formatted;
    }
}





package log4j.util;

import java.util.*;

import org.apache.log4j.Logger;
import org.apache.log4j.or.RendererMap;
import org.apache.log4j.spi.*;
import org.apache.log4j.xml.XMLLayout;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.helpers.Transform;

public class MDCAwareXMLLayout extends XMLLayout
{
    public static final Logger LOGGER = Logger.getLogger(MDCAwareXMLLayout.class);

    private RendererSupport rendererSupport;

    public MDCAwareXMLLayout ()
    {
        super();
    }

    public String getRendererSupportClass ()
    {
        return (rendererSupport == null)
            ? null
            : rendererSupport.getClass().getName();
    }

    public void setRendererSupportClass (String className)
    {
        try
        {
            Class c = Class.forName(className, false, Thread.currentThread().getContextClassLoader());
            if (RendererSupport.class.isAssignableFrom(c))
            {
                rendererSupport = (RendererSupport)c.newInstance();
            }
            else
            {
                LogLog.warn(className + " does not implement " + RendererSupport.class.getName());
            }
        }
        catch (ClassNotFoundException cnfe)
        {
            LogLog.warn("Could not find class " + className, cnfe);
        }
        catch (IllegalAccessException illEx)
        {
            LogLog.warn("Could not create instance of class " + className, illEx);
        }
        catch (InstantiationException isAbstract)
        {
            LogLog.warn("Can not create instance of class " + className, isAbstract);
        }
        catch (ExceptionInInitializerError uhOh)
        {
            LogLog.warn("Could not initialize class " + className, uhOh);
        }
        catch (SecurityException secEx)
        {
            LogLog.warn("Not allowed to create instance of class " + className, secEx);
        }
    }

    public String format (LoggingEvent event)
    {
        String formatted = super.format(event);

        if (event instanceof OpenLoggingEvent)
        {
            OpenLoggingEvent openEvent =(OpenLoggingEvent)event;
            Collection mdcKeys = new TreeSet(openEvent.getMDCKeys());
            if (mdcKeys.size() > 0)
            {
                StringBuffer buf = new StringBuffer(formatted);
                buf.setLength(buf.indexOf("</log4j:event") + 1);
                buf.append("<log4j:mdc>\r\n");

                RendererMap rendererMap = null;
                if (openEvent.getLogger() != null)
                {
                    LoggerRepository maybeRendererSupport = openEvent.getLogger().getLoggerRepository();
                    if (maybeRendererSupport instanceof RendererSupport)
                    {
                        rendererMap = ((RendererSupport)maybeRendererSupport).getRendererMap();
                    }
                    else
                    {
                        if (LOGGER.getLoggerRepository() instanceof RendererSupport)
                        {
                            rendererMap = ((RendererSupport)LOGGER.getLoggerRepository()).getRendererMap();
                        }
                    }
                }
                if (rendererMap == null && rendererSupport != null)
                {
                    rendererMap = rendererSupport.getRendererMap();
                }

                for (Iterator keys = mdcKeys.iterator(); keys.hasNext();)
                {
                    String nextKey = (String)keys.next();
                    Object nextValue = event.getMDC(nextKey);
                    String val = (rendererMap == null)
                        ? String.valueOf(nextValue)
                        : rendererMap.findAndRender(nextValue);
                    buf.append("<log4j:mdcEntry key=\"")
                        .append(Transform.escapeTags(nextKey))
                        .append("\" valueClass=\"")
                        .append(nextValue.getClass().getName())
                        .append("\" renderedValue=\"")
                        .append(Transform.escapeTags(val))
                        .append("\"/>\r\n");
                }

                buf.append("</log4j:mdc>\r\n");
                buf.append("</log4j:event>\r\n\r\n");
                formatted = buf.toString();
            }
        }
        return formatted;
    }
}

No comments:

Post a Comment