3

I have 2 FileHandlers that write out to two separate files, and the amount of I/O occurring is slowing down my application quite a bit:

  1. I've decided to have the FileHandlers run on separate threads.

  2. As they are on separate threads, I needed a concept of a "queue", so that these separate threads can poll this queue and print out any incoming messages.

  3. I've preformatted messages so that any arguments used do not change before they actually reach the print out in the FileHandler.

  4. Now I've realised that I can't use the "log" methods provided by the logger, as that attempts to invoke methods on the current thread to format and print out the messages.

  5. So I simply call a method that adds my trace message to the queue.

  6. I then use run() method of the FileHandlers to print out the messages using publish().

  7. I now realise publish() only takes in a LogRecord, which is just a level + message.

  8. My traces have much more, which can't simply be placed in an overall message, I want to be able to use the "Formatter" that I've set to the FileHandler.

  9. So I create an instance of the logger in the FileHandler, so I can use the log method and format the string as designed in the Formatter.

Which works, kinda.

...

This is getting a bit silly, is it worth continuing on like this, working AROUND the java.util.Logger rather than working with it? One of the useful parts of the java.util.Logger is having a separate logger instance for each class and being able to have more control over the messages...

Any suggestions?

The code is quite long, but I think it's easily understood from the description above, if not let me know and I'll upload somewhere.

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
sro
  • 71
  • 1
  • 9

3 Answers3

5

If I/O is truly the bottleneck and you don't need file rotation and file locking then create a Handler that queues the fully formatted output string/bytebuffer from your LogRecord + "trace message". Then hand off/queue the fully formatted output string/bytebuffer to a thread to perform the I/O.

Otherwise, if you need to use the FileHandler and want to pass a LogRecord + your trace to the publish method you can just subclass the FileHandler and then create a mapping between your LogRecord and trace that is visible to your custom formatter. A few ways to do that are:

  1. Create a Map<LogRecord, Trace> visible to both the handler and formatter.
  2. Create a LogRecord subclass to hold the trace and convert each LogRecord to your new subclass and super.publish the LogRecord subclass. Then cast each LogRecord in your formatter to access the trace.

4.Now I've realised that I can't use the "log" methods provided by the logger, as that attempts to invoke methods on the current thread to format and print out the messages.

Logger.log creates LogRecords and invokes handler.publish for the attached handlers and parent handlers by default. It is handler.publish that is performing the I/O on the current thread. What you have to do is remove all handlers that perform I/O on publish and replace them with handlers that just queue LogRecords on publish.

Here is an example of how to create an AsyncFileHandler:

    public class AsyncFileHandler extends FileHandler implements Runnable {

        private static final int offValue = Level.OFF.intValue();
        private final BlockingQueue<LogRecord> queue = new ArrayBlockingQueue<>(5000);
        private volatile Thread worker;

        public AsyncFileHandler() throws IOException {
            super();
        }

        public AsyncFileHandler(String pattern, int limit, int count, boolean append)
                throws IOException {
            super(pattern, limit, count, append);
        }

        @Override
        public void publish(LogRecord record) {
            int levelValue = getLevel().intValue();
            if (record.getLevel().intValue() < levelValue || levelValue == offValue) {
                return;
            }

            final Thread t = checkWorker();
            record.getSourceMethodName(); //Infer caller.
            boolean interrupted = Thread.interrupted();
            try {
                for (;;) {
                    try {
                        boolean offered = queue.offer(record, 10, TimeUnit.MILLISECONDS);
                        if (t == null || !t.isAlive()) {
                            if (!offered || queue.remove(record)) {
                                handleShutdown(record);
                            }
                            break;
                        } else {
                            if (offered || handleFullQueue(record)) {
                                break;
                            }
                        }
                    } catch (InterruptedException retry) {
                        interrupted = true;
                    }
                }
            } finally {
                if (interrupted) {
                    Thread.currentThread().interrupt();
                }
            }
        }

        private boolean handleFullQueue(LogRecord r) {
            super.publish(r);
            return true; //true if handled.
        }

        private void handleShutdown(LogRecord r) {
            super.publish(r);
        }

        @Override
        public void close() {
            try {
                try {
                    final Thread t = this.worker;
                    if (t != null) {
                        t.interrupt();
                        shutdownQueue();
                        t.join();
                        shutdownQueue();
                    }
                } finally {
                    super.close();
                }
            } catch (InterruptedException reAssert) {
                Thread.currentThread().interrupt();
            }
        }

        private void shutdownQueue() {
            for (LogRecord r; (r = queue.poll()) != null;) {
                handleShutdown(r);
            }
        }

        @Override
        public void run() {
            try {
                final BlockingQueue<LogRecord> q = this.queue;
                for (;;) {
                    super.publish(q.take());
                }
            } catch (InterruptedException shutdown) {
                shutdownQueue();
                Thread.currentThread().interrupt();
            }
        }

        private Thread checkWorker() {
            Thread t = worker;
            if (t == null) {
                t = startWorker();
            }
            return t;
        }

        private synchronized Thread startWorker() {
            if (worker == null) {
                worker = Executors.defaultThreadFactory().newThread(this);
                worker.setDaemon(true); //JDK-8060132
                worker.setContextClassLoader(getClass().getClassLoader());
                worker.start();
            }
            return worker;
        }
    }

There is advice in the LogRecord documentation which even the original authors fail to follow in the MemoryHandler. It reads as the following:

Therefore, if a logging Handler wants to pass off a LogRecord to another thread, or to transmit it over RMI, and if it wishes to subsequently obtain method name or class name information it should call one of getSourceClassName or getSourceMethodName to force the values to be filled in.

So if you are going to buffer LogRecords in a queue you have to call getSourceClassName or getSourceMethodName before you add the records to the queue. Otherwise your log will record the wrong source class and source method names.

jmehrens
  • 10,580
  • 1
  • 38
  • 47
  • Hey, thanks for the input, the FileHandler class provides some useful options that I want to be configurable (rotation, append etc), so the second approach seems more suited. 4.Now I've realised that I can't use the "log" methods provided by the logger, as that attempts to invoke methods on the current thread to format and print out the messages. I meant that if I were to call the ".log()" method from the main thread, that would end up carrying out the I/O, rather than on a separate thread which I'd solve by doing the queue/separate thread approach. – sro Dec 10 '14 at 20:23
  • 1
    Logger.log just creates log records and publishes them. It is handler.publish that is performing the I/O. So you can decouple I/O from logger.log by performing the hand off in handler.publish. – jmehrens Dec 10 '14 at 20:43
  • Most of that is code you didn't have to write. The proper answer to the original question is to use a thread pool of one thread per file handler. `ExecutorService executor = Executors.newSingleThreadExecutor();` People might wonder why would anyone use a thread pool of one thread. Because of the managed queue you don't see and don't have to worry about. The only time I want to be dealing with raw threads and interrupting them is never. – Martin F Apr 26 '21 at 23:23
  • @MartinF `Executors.newSingleThreadExecutor()` creates non-daemon threads. Daemon threads are used here to ensure the handler can't prevent application shutdown. I could reduce the code size if I just submitted every log record to the executor. However, that creates a lot of garbage and memory pressure. STE is unbounded but this handler needs to put back pressure on the producer and avoid using the CallerRunsPolicy. – jmehrens Apr 27 '21 at 15:38
  • @jmehrens @jmehrens * Your daemon thread will get interrupted right away on shutdown and I don't see how that differs from calling `shutdownNow` on a thread pool rather than `shutdown` in your `Runtime.getRuntime().addShutdownHook()` hook. * I don't think it's reasonable to believe that logs will queue disproportionately if at all, especially if your code makes network calls. I'm not sure that pushing back and holding up threads sporadically is the answer to that anyway. – Martin F May 20 '21 at 20:05
  • @jmehrens I forgot that you can actually specify a custom queue when creating an executor. – Martin F May 20 '21 at 20:28
  • @MartinF It is possible to leak handlers where close/shutdownXXX won't get called. I don't want a dangling thread to hold up shutdown of the JVM. I'm not using the executor with a custom queue because that wraps every log record in a FutureTask + Callable. Why not just post a competing answer? – jmehrens May 20 '21 at 21:51
  • @jmehrens Leaky Executors... OK, now you are just making up things. There are very few reasons to use raw threads on production when writing java. Zero even. – Martin F May 21 '21 at 21:43
  • @MartinF Here is the exact leak I'm referring to: [JDK-8060132 - Handlers configured on abstract nodes in logging.properties are not always properly closed](https://bugs.openjdk.java.net/browse/JDK-8060132). If Handler leaks then Handler.close is not called which means shutdown is not called. Totally agree with you. Executors are awesome and there are few reasons to use raw. I explained the reasons I didn't use executors here and treated this as the rare case to use threads. – jmehrens May 21 '21 at 22:26
2

You should really consider using SLF4J.

These issues you are describing come along with rolling your own logger. SLF4J is really commonly used because it is not very intrusive, should you decide that you need something different you can swap it out for a different framework.

http://saltnlight5.blogspot.ca/2013/08/how-to-configure-slf4j-with-different.html

http://www.slf4j.org/manual.html

If you do decide to stick with your own logger I would say that you should start by making it a singleton with several logwriters attached to log levels, you then setup a non-blocking queue for each logger(there are tons of examples of this out there) and simply call log(logLevel, logOrigin, logMessage) and under the hood send this to each logwriter, which would be a non-blocking queue that runs on a thread for each logwriter.

Each logwriter should be it's own thread, not each log, and you only need one logger as all it is is a short hand to place things in your logwriter's queues from anywhere in your app.

davidahines
  • 3,976
  • 16
  • 53
  • 87
  • Hey dah, thanks for the response. When you say "simply call log(logLevel, logOrigin, logMessage)", this is a method I'd implement myself right? I wouldn't actually use the Java util logger's method for log() as that ends up printing to the handler in the current thread. I think I currently have it as you described. A singleton with several logwriters attached to log levels, each with a queue, whenever I want to log a message I call a method MyLogger.log(...), which then pushes it onto the logwriters. Each logwriter is then on it's own thread that prints these out. – sro Dec 10 '14 at 19:36
  • Yea, what I mean is that you basically only have a single logger in your whole app that just pushes it off to the logwriters, they write the logs when they have time. I think this is a fine solution, but to be honest, SLF4J does a lot of this for you and can be setup quickly. – davidahines Dec 10 '14 at 20:12
1

You can use log4j 2 JDK Logging Adapter to enable logging with log4j, And log4j 2 provide remarkable asynchronous Logging mechanism with lot of configuration options.

Necessary VM arguments

-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager
-DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector

You can find more information about log4j 2 async logging here and tomcat configuration details here

sample Log4j2.xml for async loggign

<Configuration status="WARN">
  <Appenders>
    <!-- Async Loggers will auto-flush in batches, so switch off immediateFlush. -->
    <RandomAccessFile name="RandomAccessFile" fileName="async.log" immediateFlush="false" append="false">
      <PatternLayout>
        <Pattern>%d %p %c{1.} [%t] %m %ex%n</Pattern>
      </PatternLayout>
    </RandomAccessFile>
  </Appenders>
  <Loggers>
    <Root level="info" includeLocation="false">
      <AppenderRef ref="RandomAccessFile"/>
    </Root>
  </Loggers>
</Configuration>
Tharinda Wicky
  • 388
  • 4
  • 16