How do I redirect log4j output to my HttpServletResponse output stream?
Asked Answered
P

4

6

I'm using log4j 1.2.15 in a Spring 3.1.1.RELEASE application deployed on JBoss AS 7.1.1.Final. I'm trying to route output written in log4j to my response output stream. I have output written like this

private static final Logger LOG = Logger.getLogger(TrainingSessionServiceImpl.class);
…
LOG.info("Creating/updating training session associated with order #:" + order.getId());

and I'm trying to route it to my output stream like so …

@RequestMapping(value = "/refreshPd", method = RequestMethod.GET)
public void refreshPD(final HttpServletResponse response) throws IOException
{
    ...        
    final WriterAppender appender = new WriterAppender(new PatternLayout("%d{ISO8601} %p - %m%n"),response.getWriter());
    appender.setName("CONSOLE_APPENDER");
    appender.setThreshold(org.apache.log4j.Level.DEBUG);
    Logger.getRootLogger().addAppender(appender);

    worker.work();

    Logger.getRootLogger().removeAppender("CONSOLE_APPENDER");

but sadly, nothing is getting output to my browser, even though I know (through debugging) that logging statements are getting called. Does anyone know how I can adjust my setup to make it work? Below is my log4j.properties file, deployed to my wAR's WEB-INF/classes directory.

log4j.rootLogger=DEBUG, CA, FA

#Console Appender
log4j.appender.CA=org.apache.log4j.ConsoleAppender
log4j.appender.CA.layout=org.apache.log4j.PatternLayout
log4j.appender.CA.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n

#File Appender
log4j.appender.FA=org.apache.log4j.FileAppender
log4j.appender.FA.File=/usr/java/jboss/server/default/log/log4j.log
log4j.appender.FA.layout=org.apache.log4j.PatternLayout
log4j.appender.FA.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n

# Set the logger level of File Appender to WARN
log4j.appender.FA.Threshold = DEBUG

Thanks, - Dave

Phyto answered 18/1, 2013 at 22:41 Comment(4)
Does worker.work() run in same or other thread? Problem suggests that it runs in another thread.Davide
Yes, it runs in the same thread.Phyto
Works fine for me on Tomcat. Upon a bit investigation, it look like a JBoss AS specific issue: #6072309 and subsequently issues.jboss.org/browse/JBAS-9318Davide
This seems like a really bad idea. If you're adding an appender to the root logger (shared by all threads), you're just asking for a mish-mash of log messages as multiple requests come into the app server. Please see my answer below for a suggested implementation that I think is ultimately safer / more maintainable.Enamor
B
7

This was an interesting problem. The key thing is to write your own appender. I looked up the in built org.apache.log4j.ConsoleAppender code for inspiration. I have tested this in my tomcat and verified that it works. I used log4j-1.2.17 (hopefully shouldn't matter)

1) First implement your own appender. This appender will write all log events to current thread's outputstream

package com.tstwbprj.log;

import org.apache.log4j.Layout;
import org.apache.log4j.WriterAppender;

import java.io.IOException;
import java.io.OutputStream;

public class HttpLogAppender extends WriterAppender {

    static ThreadLocal<OutputStream> streamPerHttpThread = new ThreadLocal<OutputStream>();

    public HttpLogAppender() {

    }

    public HttpLogAppender(Layout layout) {
        setLayout(layout);       //super-class method
        activateOptions();
    }

    public void setCurrentHttpStream(OutputStream stream) {
        streamPerHttpThread.set(stream);
    }


    public void activateOptions() {
        setWriter(createWriter(new CurrentHttpThreadOutStream()));
    }


    /**
     * An implementation of OutputStream that redirects to the
     * current http threads servlet output stream
     */
    private static class CurrentHttpThreadOutStream extends OutputStream {
        public CurrentHttpThreadOutStream() {
        }

        public void close() {
        }

        public void flush() throws IOException {
            OutputStream stream = streamPerHttpThread.get();
            if (stream != null) {
                stream.flush();
            }
        }

        public void write(final byte[] b) throws IOException {
            OutputStream stream = streamPerHttpThread.get();
            if (stream != null) {
                stream.write(b);
            }
        }

        public void write(final byte[] b, final int off, final int len)
                throws IOException {
            OutputStream stream = streamPerHttpThread.get();
            if (stream != null) {
                stream.write(b, off, len);
            }
        }

        public void write(final int b) throws IOException {
            OutputStream stream = streamPerHttpThread.get();
            if (stream != null) {
                stream.write(b);
            }
        }
    }
}

2) Add this appender in your log4j configuration file just like the other settings

log4j.rootLogger=DEBUG, CA, FA , HA
..
log4j.appender.HA=com.tstwbprj.log.HttpLogAppender log4j.appender.HA.layout=org.apache.log4j.PatternLayout log4j.appender.HA.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n

3) Add a small piece of code in your servlet so that this appender works correctly . Here's my servlet.

import org.apache.log4j.Category;
import org.apache.log4j.Logger;
import javax.servlet.ServletOutputStream;
import java.io.IOException;

public class LogServlet extends javax.servlet.http.HttpServlet {

    private static final Logger LOG = Logger.getLogger(LogServlet.class);

    protected void doPost(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException, IOException {

    }

    protected void doGet(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException, IOException {
        ServletOutputStream outstream = response.getOutputStream();
        configureLogForCurrentRequest(outstream);

        LOG.info("Got request");//this is now send to the servlet output stream !!
        LOG.info("Hello!!");
        LOG.info("Done!!");
    }

    private void configureLogForCurrentRequest(ServletOutputStream outstream) {

        HttpLogAppender appender = (HttpLogAppender) LOG.getAppender("HA");
        while (appender == null) {
            Category parent = LOG.getParent();
            if (parent == null) {
                break; //This ideally shouldn't happen. Navigated all the way to root logger and still did not find appender !!..something wrong with log4j configuration setup
            }
            appender = (HttpLogAppender) parent.getAppender("HA");

        }
        appender.setCurrentHttpStream(outstream);
    }
}

Caution : This is not thoroughly tested especially with multiple servlet requests etc. Also not sure why you want to do this. Its not typical to pipe log messages to browser. Proceed with caution..:)-

Badlands answered 25/1, 2013 at 11:6 Comment(5)
Hi, I'm getting a NullPointerException at the line "appender.setCurrentHttpStream(outstream);". I notice that when "HttpLogAppender appender = (HttpLogAppender) LOG.getAppender("HA");" is called, appender is also initially null. What are you trying to do with "configureLogForCurrentRequest"?Phyto
The HttpLogAppender should be associated with your root logger if your log4j.properties is setup correctly...It will be null in that first line. Thats why we have the while loop to go up the logger hierarchy to retrieve it from the root logger. Once HttpLogAppender is retrieved , we pass it the current servlets's response outputstream so that any logging event is written to itBadlands
I discovered that JBoss messes with log4j worse than a Penn St defensive coordinator -- https://mcmap.net/q/807020/-using-log4j-with-jboss-7-1. Once I added that deployment structure file, you're solution worked. Thanks, -Phyto
FWIW I took this idea and made a more general version of the same thing. Available here.Bilbe
Extending WriterAppender is the right answer but do be aware of the implementation as documented for append(LoggingEvent) ... there is no buffering, no mention of capacity backlog limits, etc... "If the output stream exists and is writable then write a log statement to the output stream. Otherwise, write a single warning message to System.err." logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/…Gmur
E
1

Try with something like this:

Logger logger = Logger.getRootLogger();
String name = "myAppender";

Appender servletAppender = logger.getAppender(appenderName);
OutputStream out = response.getOutputStream();

if (servletAppender == null) {
    servletAppender = new WriterAppender(new PatternLayout("%d{ISO8601} %p - %m%n"), out);
    servletAppender.setName(appenderName);
    appender.setThreshold(org.apache.log4j.Level.DEBUG);
    logger.addAppender(servletAppender);
}

try {
    // Your work
    worker.work();
} finally {
    logger.removeAppender(appenderName);
    out.flush();
}
Endodermis answered 23/1, 2013 at 19:10 Comment(1)
There doesn't seem to be a "setThreshold" method for the org.apache.log4j.Appender class. I commented the "setThreshold" line out, but still no dice -- nothing is written to the browser. Interestingly, the log output is written to my JBoss 7.1.1 server log.Phyto
W
0

I suggest to take alternative approach and fetch log file contents to separate browser tab.

This would not require main code modification and would not destroy original page's formatting.

Some web-based log file viewers links:

Wanderjahr answered 23/1, 2013 at 18:58 Comment(0)
E
0

Not a precise answer as such, but a better way that I have seen this handled is to write your own Appender that will collect logs in a ThreadLocal. At the time your servlet request completes, you can drain the contents of the ThreadLocal and output to the response stream however you wish.

This satisfies the (unstated) requirement of thread safety, and can fairly cleanly isolate the log4j (or other logging framework) implementation code (which should be small, using this technique) from the manipulation of the ThreadLocal, which could in theory be reused in other areas of your code.

This type of technique is used by many server-side scripting languages such as ColdFusion and others.

I won't go into the potential bugs you could cause with inappropriate use of ThreadLocal in an app server, there are techniques to manage this, along with relevant answers on SO and other sites.

Hope this answer might redirect your thinking in a slightly different direction!

Enamor answered 24/1, 2013 at 17:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.