Log4J Appender to create per-request logs
authorgilbert <gilbert@ab3bd59b-922f-494d-bb5f-6f0a3c29deca>
Thu, 28 Oct 2004 13:13:00 +0000 (13:13 +0000)
committergilbert <gilbert@ab3bd59b-922f-494d-bb5f-6f0a3c29deca>
Thu, 28 Oct 2004 13:13:00 +0000 (13:13 +0000)
git-svn-id: https://subversion.switch.ch/svn/shibboleth/java-idp/trunk@1139 ab3bd59b-922f-494d-bb5f-6f0a3c29deca

src/edu/internet2/middleware/commons/log4j/RequestLoggingFilter.java [new file with mode: 0644]
src/edu/internet2/middleware/commons/log4j/ShowLog.java [new file with mode: 0644]
src/edu/internet2/middleware/commons/log4j/SimpleAppenderContextImpl.java [new file with mode: 0644]
src/edu/internet2/middleware/commons/log4j/ThreadLocalAppender.java [new file with mode: 0644]
src/edu/internet2/middleware/commons/log4j/ThreadLocalAppenderContext.java [new file with mode: 0644]
src/edu/internet2/middleware/commons/log4j/WrappedLog.java [new file with mode: 0644]

diff --git a/src/edu/internet2/middleware/commons/log4j/RequestLoggingFilter.java b/src/edu/internet2/middleware/commons/log4j/RequestLoggingFilter.java
new file mode 100644 (file)
index 0000000..d9ef991
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ * RequestLoggingFilter.java
+ * 
+ * Configure any Servlet context that you want to trace setting
+ * this class as a filter in the WEB-INF/web.xml
+ * 
+ *     <filter>
+ *             <filter-name>RequestLogFilter</filter-name>
+ *             <filter-class>edu.internet2.middleware.commons.log4j.RequestLoggingFilter</filter-class>
+ *     </filter>
+ * 
+ * The default is to use SimpleAppenderContextImpl as the helper class
+ * for the Log4J ThreadLocalAppender. If you want to use another
+ * class, specify its name in the filter config as
+ * 
+ *     <init-param>
+ *             <param-name>appenderContextClass</param-name>
+ *             <param-value>[fill class name in here]</param-value>
+ *     </init-param>
+ *
+ * The name of the class specified here must match the name
+ * configured to the LocalContext property of the ThreadLocalAppender
+ * in the Log4J configuration file.
+ * 
+ * This Filter calls the startRequest() and endRequest() methods
+ * of the helper class object to start and stop tracing for the 
+ * Servlet request. At the end it takes the buffer of data and 
+ * saves it to a named attribute of the HttpSession object. 
+ * 
+ * You can, of course, use this as a model for other code that
+ * processes the trace data differently.
+ * 
+ * Dependencies: Log4J
+ * 
+ * --------------------
+ * Copyright 2002, 2004 
+ * Yale University
+ * University Corporation for Advanced Internet Development, Inc. 
+ * All rights reserved
+ * Your permission to use this code is governed by "The Shibboleth License".
+ * A copy may be found at http://shibboleth.internet2.edu/license.html
+ */
+package edu.internet2.middleware.commons.log4j;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+/**
+ * @author Howard Gilbert
+ */
+public class RequestLoggingFilter implements Filter {
+    
+    private static final String FilterInitParamName = "appenderContextClass";
+    public static final String REQUESTLOG_ATTRIBUTE = "edu.internet2.middleware.commons.log4j.requestlog";
+    ThreadLocalAppenderContext ctx = new SimpleAppenderContextImpl();
+
+    /**
+     * Extract the helper class name init param (if provided) and create an
+     * object of the class.
+     * 
+     * <p>If the class cannot be found or the object cannot be created,
+     * print a message but do nothing more.</p>
+     */
+    public void init(FilterConfig filterConfig) throws ServletException {
+        ThreadLocalAppenderContext newctx = null;
+           String appenderContextClassname = filterConfig.getInitParameter(FilterInitParamName);
+           if (appenderContextClassname==null)
+               return;
+           try {
+            Class appenderContextClass = Class.forName(appenderContextClassname);
+            Object o = appenderContextClass.newInstance();
+            if (o instanceof ThreadLocalAppenderContext)
+                newctx = (ThreadLocalAppenderContext) o;
+        } catch (ClassNotFoundException e) {
+        } catch (InstantiationException e) {
+        } catch (IllegalAccessException e) {
+        }
+        if (newctx!=null)
+            ctx=newctx;
+        else
+            System.out.println("appenderContext parameter specified invalid classname");
+    }
+
+    /**
+     * For every Http request processed through this context (and
+     * mapped by the Filter mapping to this filter) enable thread local
+     * request logging on the way in and collect the log data on the way out.
+     */
+    public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain chain) throws IOException, ServletException {
+        if (!(arg0 instanceof HttpServletRequest)) {
+            chain.doFilter(arg0, arg1); // only handle HTTP requests
+        }
+        HttpServletRequest request = (HttpServletRequest) arg0; 
+        HttpServletResponse response = (HttpServletResponse) arg1;
+        
+        if (ctx==null) {
+            chain.doFilter(arg0,arg1); // do the request while logging
+            return;
+        }
+            
+        ctx.startRequest(); // start logging
+        
+        try {    
+            chain.doFilter(arg0,arg1); // do the request while logging
+        } finally {
+            WrappedLog log = ctx.endRequest(); // stop logging, get the data
+            
+            // Now put the data in a Session attribute
+            if (log!=null) {
+                HttpSession session = request.getSession();
+                if (session!=null) {
+                    session.setAttribute(REQUESTLOG_ATTRIBUTE, log);
+                }
+            }
+        }
+    }
+
+    /**
+     * 
+     */
+    public void destroy() {
+        
+    }
+
+}
diff --git a/src/edu/internet2/middleware/commons/log4j/ShowLog.java b/src/edu/internet2/middleware/commons/log4j/ShowLog.java
new file mode 100644 (file)
index 0000000..ce68abd
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * ShowLog.java
+ * 
+ * Servlet that extracts the ThreadLocal log data from the HttpSession and 
+ * returns it to the user's browser.
+ * 
+ * Dependencies: The session attribute name must match the name used by the Filter.
+ * 
+ * --------------------
+ * Copyright 2002, 2004 
+ * Yale University
+ * University Corporation for Advanced Internet Development, Inc. 
+ * All rights reserved
+ * Your permission to use this code is governed by "The Shibboleth License".
+ * A copy may be found at http://shibboleth.internet2.edu/license.html
+ */
+package edu.internet2.middleware.commons.log4j;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+/**
+ * @author Howard Gilbert
+ */
+public class ShowLog extends HttpServlet {
+    public static final String REQUESTLOG_ATTRIBUTE = "edu.internet2.middleware.commons.log4j.requestlog";
+
+    public void doGet(HttpServletRequest request, HttpServletResponse response)
+            throws ServletException, IOException {
+        HttpSession session = request.getSession();
+        if (session!=null) {
+            WrappedLog logBuffer = (WrappedLog) session.getAttribute(REQUESTLOG_ATTRIBUTE);
+            response.setContentType("text/plain");
+            Writer out = response.getWriter();
+            if (logBuffer==null)
+                out.write("No Log Data");
+            else
+                out.write(logBuffer.getLogData());
+        }
+        
+    }
+}
diff --git a/src/edu/internet2/middleware/commons/log4j/SimpleAppenderContextImpl.java b/src/edu/internet2/middleware/commons/log4j/SimpleAppenderContextImpl.java
new file mode 100644 (file)
index 0000000..2949970
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * SimpleAppenderContext.java
+ * 
+ * A TheadLocalAppenderContext implementation class serves as the 
+ * meeting point between a particular instance of ThreadLocalAppender
+ * in a Log4J configuration and a particular thread pool request 
+ * dispatcher (such as a Tomcat application context). 
+ * 
+ * In this simple case, the ThreadLocal reference is held in a 
+ * static variable in this class, and it points to a StringWriter.
+ * 
+ * The ThreadLocalAppender is configured (or defaults since this
+ * is the default value) through the LocalContext property. Set 
+ * that property in the Log4J configuration file with the name of 
+ * a class that implements ThreadLocalAppenderContext. 
+ * 
+ * The request container is also configured with or default do the
+ * name of this class. It calls startRequest() when a new request
+ * arrives and endRequest() before returning from request processing.
+ * An example is the RequestLoggingFilter that makes these calls just
+ * before and just after chaining a Servlet GET or POST request on to
+ * the next Filter/Servlet in the processing chain.
+ * 
+ * What ties things together is the name of this class, and the 
+ * fact that the ThreadLocal variable is static in this class. So
+ * if you want two differently configured ThreadLocalAppenders to share
+ * the same JVM ClassLoader, then you have to create two different classes
+ * with two different names and configure at least one new name as the
+ * Log4J Appender property or the Filter initialization parameter.
+ * 
+ * Note: The ThreadLocalAppender creates one object of this class.
+ * The RequestLoggingFilter creates a separate object. The two
+ * objects share only the static variable. Do not make the 
+ * mistake of assuming that the Filter and log share the same
+ * object.
+ * 
+ * --------------------
+ * Copyright 2002, 2004 
+ * Yale University
+ * University Corporation for Advanced Internet Development, Inc. 
+ * All rights reserved
+ * Your permission to use this code is governed by "The Shibboleth License".
+ * A copy may be found at http://shibboleth.internet2.edu/license.html
+ */
+package edu.internet2.middleware.commons.log4j;
+
+import java.io.StringWriter;
+import java.io.Writer;
+
+
+/**
+ * @author Howard Gilbert
+ */
+public class SimpleAppenderContextImpl 
+       implements ThreadLocalAppenderContext {
+    
+    private static ThreadLocal localWriterReference = new ThreadLocal();
+
+    /**
+     * @return Null or the Writer for the current thread.
+     */
+    public Writer getLocalWriter() {
+        return (Writer) localWriterReference.get();
+    }
+
+    /**
+     * Called to signal the start of Request processing for this thread.
+     */
+    public void startRequest() {
+        localWriterReference.set(new StringWriter());
+    }
+
+    /**
+     * Called to signal the end of Request processing. Return log data
+     * and null out the Writer to stop collecting data.
+     * 
+     * @return A wrapped String containing the log data.
+     */
+    public WrappedLog endRequest() {
+        StringWriter stringWriter =(StringWriter) localWriterReference.get();
+        if (stringWriter==null)
+            return null;
+        String logdata = stringWriter.toString();
+        localWriterReference.set(null);
+        return new WrappedStringLog(logdata);
+    }
+    
+    
+    /**
+     * The log Writer could be a file or WebDav network store. So
+     * the log data could be a String, or a file name, or a URL.
+     * This class handles the simple String case.
+     * @author Howard Gilbert
+     */
+    static class WrappedStringLog implements WrappedLog {
+        
+        String logdata;
+        
+        WrappedStringLog(String logdata) {
+            this.logdata=logdata;
+            
+        }
+        
+        public String getLogData() {
+            return logdata;
+        }
+        
+    }
+
+}
diff --git a/src/edu/internet2/middleware/commons/log4j/ThreadLocalAppender.java b/src/edu/internet2/middleware/commons/log4j/ThreadLocalAppender.java
new file mode 100644 (file)
index 0000000..1fbd090
--- /dev/null
@@ -0,0 +1,155 @@
+/*
+ * ThreadLocalAppender.java
+ * 
+ * This is a Log4J Appender. You add it to your Log4J configuration just 
+ * like any other appender class. However, it doesn't write the log data
+ * to one file or socket like the other appenders. It obtains a Writer
+ * from a companion class.
+ * 
+ * We need a companion class to mediate between the Log4J conventions
+ * and some Container environment that is dispatching requests to classes
+ * using a worker thread pool. Tomcat is a simple example of such a container.
+ * This class doesn't know about Tomcat or any other container. 
+ * 
+ * An object of this class is created whenever an Appender of this
+ * type is added (by program or configuration file) to a Log4J logger.
+ * There may be more than one "logger" (that is, there may be more
+ * than one point in the category name hierarchy of "a.b.c.d" and
+ * each with different levels of logging (DEBUG, INFO) to which 
+ * thread local request logging is attached. An event will be logged
+ * from any of these sources that pass the level criteria. Here, as
+ * with the rest of the Log4J environment, the real logging is based
+ * on static fields.
+ * 
+ * However, and this is a key feature of the logic, this "static"
+ * environment is ThreadLocal. That means that this "static" data
+ * really has a different reference and points to a different Writer
+ * in each request processing thread. This is why the superficially
+ * "static" value doesn't have to be synchronized.
+ * 
+ * All ThreadLocalAppenders that share the same companion class name
+ * share the same output buffer. To create a separate buffer with
+ * separate data, you need both another companion class (which can
+ * be modelled on SimpleAppenderContextImpl, but must have a different
+ * name) and a separate Filter to load and activate it.
+ * 
+ * Dependencies: Log4J
+ * 
+ * --------------------
+ * Copyright 2002, 2004 
+ * Yale University
+ * University Corporation for Advanced Internet Development, Inc. 
+ * All rights reserved
+ * Your permission to use this code is governed by "The Shibboleth License".
+ * A copy may be found at http://shibboleth.internet2.edu/license.html
+ */
+package edu.internet2.middleware.commons.log4j;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import org.apache.log4j.AppenderSkeleton;
+import org.apache.log4j.spi.LoggingEvent;
+
+/**
+ * Appender that writes to ThreadLocal storage.
+ * 
+ * <p>This is a standard Log4J appender that just happens to get a Writer
+ * every time it wants to log data from a companion class. Actually, this
+ * class doesn't know anything about ThreadLocal, but the motivation for
+ * this is to maintain separate easy to access logs for each request, and 
+ * that can only be accomplied with a ThreadLocal Writer.</p>
+ * 
+ * <p> Everything here is defined by the Log4J API.</p>
+ * 
+ * @author Howard Gilbert
+ */
+public class ThreadLocalAppender extends AppenderSkeleton{
+
+    private ThreadLocalAppenderContext appenderContext = new SimpleAppenderContextImpl();
+    
+    /**
+     * A String property that can be set by the Log4J configuration file 
+     * for this Appender to provide the name of a different companion 
+     * class implementing the necessary interfaces.
+     * <p>
+     * Although it is not obvious from any explicit documentation,
+     * when Log4J loads an Appender class it uses Bean Introspection
+     * to determine any properties of the bean. Subsequent statements
+     * in the configuration file (property or xml) can then specify
+     * values for the property. In this case, a property named 
+     * "LocalContext" can be set to the name of a class that implements
+     * the ThreadLocalAppenderContext interface.
+     * </p><p>
+     * If the property is not set, then "SimpleAppenderContextImpl" is used.</p>
+     */
+    private String localContext = null;
+    public String getLocalContext() {
+        return localContext;
+    }
+    public void setLocalContext(String localContext) {
+        this.localContext = localContext;
+        try {
+            Class c = Class.forName(localContext);
+            if (ThreadLocalAppenderContext.class.isAssignableFrom(c)) {
+                appenderContext = (ThreadLocalAppenderContext) c.newInstance();
+            }
+        } catch (ClassNotFoundException e) { 
+        } catch (InstantiationException e) {
+        } catch (IllegalAccessException e) {
+        }
+        if (appenderContext==null)
+            System.out.println("ThreadLocalAppender cannot load "+localContext);
+    }
+    /**
+     * The main method called by Log4J when an event must be logged.
+     * 
+     * @param event
+     */
+    protected void append(LoggingEvent event) {
+        if (appenderContext==null)
+            return; // No helper class
+        Writer logBuffer = appenderContext.getLocalWriter();
+        if (logBuffer==null) {
+            // If there is no Writer, then we are probably not in a Request.
+            // Log4J is static an applies to all the code in all the classes
+            // in the source. However, some log statements will appear in 
+            // init() methods, or constructors, or background threads. If 
+            // you want to log that, you need an ordinary Log4J static 
+            // appender. This only logs stuff that happens within the
+            // processing path of a Servlet doGet() or similar request.
+            return; 
+        }
+        try {
+            logBuffer.write(this.layout.format(event));
+        } catch (IOException e) {
+            // Best effort, but will not occur with StringWriter.
+        }
+    }
+
+    public boolean requiresLayout() {
+        return true;
+    }
+
+    /**
+     * Most of the time it is OK to ignore close, but there is some
+     * chance that the Writer we are getting is associated with a 
+     * File or Socket. So just in case, forward the close on to the
+     * Writer.
+     */
+    public void close() {
+        if (appenderContext==null)
+            return;
+        Writer logBuffer = appenderContext.getLocalWriter();
+        if (logBuffer==null)
+            return;
+        try {
+            logBuffer.close();
+        } catch (IOException e) {
+            // In the common case, this is a StringBuffer that doesn't
+            // throw exceptions anyway. Otherwise, this is a best effort.
+        }
+    }
+    
+
+}
diff --git a/src/edu/internet2/middleware/commons/log4j/ThreadLocalAppenderContext.java b/src/edu/internet2/middleware/commons/log4j/ThreadLocalAppenderContext.java
new file mode 100644 (file)
index 0000000..dbf5b62
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * ThreadLocalAppenderContext.java
+ * 
+ * An interface describing the services provided by the "helper class"
+ * that feeds the ThreadLocal Writer to the Log4J ThreadLocalAppender.
+ * It also exposes startRequest() and endRequest() methods to anyone
+ * managing the gate through which the threadpool request manager 
+ * (say Tomcat) dispatches requests (GET or PUT HTTP requests) to an
+ * application (a Tomcat context) where you want a separate log file
+ * or buffer for every individual request processed.
+ * 
+ * The default implementation of this interface is provided by the
+ * SimpleAppenderContextImpl class.
+ * 
+ * --------------------
+ * Copyright 2002, 2004 
+ * Yale University
+ * University Corporation for Advanced Internet Development, Inc. 
+ * All rights reserved
+ * Your permission to use this code is governed by "The Shibboleth License".
+ * A copy may be found at http://shibboleth.internet2.edu/license.html
+ */
+package edu.internet2.middleware.commons.log4j;
+
+import java.io.Writer;
+
+
+/**
+ * Provide ThreadLocalAppender with a Writer into which to put the log data.
+ * 
+ * Provide the Request managment layer (Servlet, Servlet Filter, RMI, ...)
+ * methods to signal the start and end of a request. After the startRequest
+ * the implementing object should have generated a bucket to hold trace and
+ * should 
+ * 
+ * <p>The purpose of ThreadLocal logging is to log activity local to a request
+ * in an application server (say a Tomcat Web request). The problem is that
+ * such threads never belong to the code that is doing the logging, they 
+ * belong to the external container. So what you have to do is load the
+ * ThreadLocal reference on entry to the Servlet/EJB/whatever and then
+ * clear the pointer before returning to the container. You can't do that
+ * if the ThreadLocal variable belongs to the Appender, because the 
+ * Appender should, if properly abstracted, only know about log4j. So you
+ * have to feed the appender an object (or the name of a class that can
+ * create an object) that knows where the ThreadLocal pointer is for this
+ * application and can return it. That is what this interface does.</p>
+ * 
+ * <p>You must create a class, familiar with the environment, that implements
+ * the class and passes back either null or a ThreadLocal Writer.
+ * The name of this class must be the LocalContext parameter of the
+ * ThreadLocalAppender configuration. The class must be in the classpath when
+ * the Appender is configured.
+ * </p>
+ * 
+ * @author Howard Gilbert
+ */
+public interface ThreadLocalAppenderContext {
+    
+    /**
+     * Give the Appender a Writer into which to write data.
+     * @return Writer
+     */
+    Writer getLocalWriter();
+    
+    /**
+     * Called by the request manager (say the Servlet Filter) to 
+     * signal the start of a new request. The implementor must 
+     * allocate a new Writer to accept data.
+     */
+    void startRequest();
+    
+    /**
+     * Called by the request manager to signal the end of a request.
+     * Returns an IOU that will deliver the log data on request
+     * if it is needed (typically when a Servlet wants to display
+     * log data to a remote user.)
+     * @return WrappedLog object to access log data.
+     */
+    WrappedLog endRequest();
+    
+}
diff --git a/src/edu/internet2/middleware/commons/log4j/WrappedLog.java b/src/edu/internet2/middleware/commons/log4j/WrappedLog.java
new file mode 100644 (file)
index 0000000..1c44d15
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * WrappedLog.java
+ * 
+ * IOU object for some Log data.
+ * 
+ * This interface is implemented, for example, by the 
+ * SimpleAppenderContextImpl.WrappedStringLog class.
+ * 
+ * 
+ * --------------------
+ * Copyright 2002, 2004 
+ * Yale University
+ * University Corporation for Advanced Internet Development, Inc. 
+ * All rights reserved
+ * Your permission to use this code is governed by "The Shibboleth License".
+ * A copy may be found at http://shibboleth.internet2.edu/license.html
+ */
+package edu.internet2.middleware.commons.log4j;
+
+/**
+ * Wrapper to abstract the ThreadLocal log storage.
+ * 
+ * <p>In most cases, the log data will just be a string kept in memory.
+ * However, one could imagine it would be a file on disk, or an EhCache
+ * hybrid where the last 100 are kept in memory and the overflow of 
+ * less recently used are written to disk. So after logging is done, we
+ * return this IOU that will fetch the log data later when you want
+ * to display it.
+ * 
+ * @author Howard Gilbert
+ */
+public interface WrappedLog {
+    
+    String getLogData();
+
+}