Search code examples
javalog4j2disruptor-pattern

Log4j2 asynchronous logging is getting stuck when there is heavy logging from application


We are facing an issue with one of our applications where the peak load is causing the log4j2 to hang and the application doesn't repond. log4j2.xml

  1. We have a CustomAppender, AppMessageAppender customized for our application needs.
  2. When we have a burst in our application we are seeing almost 350 line/sec.
  3. It comes down after the short burst, but the application still doesn't respond.
  4. We have tried increasing the from default to first to 1024 and then to 2048 now. but still we see the same issue.
  5. We have the default log4j2.asyncLoggerConfigWaitStrategy set as default (Timeout).
  6. The log4j2.xml looks something like below.
    <?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" name="myApplication" packages="com.application.logging.log4j">
    <Properties>        
        <Property name="logLocation">./log</Property> <!-- current directory-->
        <Property name="filename">application</Property>
        <Property name="maxLogFileSize">50 MB</Property>
        <Property name="rolloverCount">20</Property>
    </Properties>
    <Loggers>
        <AsyncRoot level="WARN">
            <AppenderRef ref="STDOUT"/>
            <AppenderRef ref="STDERR" level="ERROR"/>
            <AppenderRef ref="File"/>
            <AppenderRef ref="FileErr" level="ERROR"/>
        </AsyncRoot>
        <AsyncLogger name="com.application" level="INFO" additivity="false">
            <AppenderRef ref="STDOUT"/>
            <AppenderRef ref="STDERR" level="ERROR"/>
            <AppenderRef ref="File"/>
            <AppenderRef ref="FileErr" level="ERROR"/>
        </AsyncLogger>
        <AsyncLogger name="CustomLogger" level="INFO" additivity="false">
            <AppenderRef ref="Message"/>
        </AsyncLogger>

        <AsyncLogger name="com.application" level="INFO" additivity="false">
            <AppenderRef ref="STDOUT"/>
            <AppenderRef ref="STDERR" level="ERROR"/>
            <AppenderRef ref="File"/>
            <AppenderRef ref="FileErr" level="ERROR"/>
        </AsyncLogger>      
    </Loggers>
    <Appenders>
        <Console name="STDOUT" target="SYSTEM_OUT">
            <PatternLayout pattern="[^%d^] [^%-5level^] [^%t^] - [^%msg^]%n"/>
        </Console>
        <Console name="STDERR" target="SYSTEM_ERR">
            <PatternLayout pattern="[^%d^] [^%-5level^] [^%t^] - [^%msg^]%n"/>
        </Console>
        <RollingRandomAccessFile name="File" fileName="${logLocation}/${filename}.log" filePattern="${logLocation}/${filename}-%d{MM-dd-yyyy}-%i.log" immediateFlush="false">
            <PatternLayout pattern="[^%d^] [^%-5level^] [^%t^] - [^%msg^]%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy />
                <SizeBasedTriggeringPolicy size="${maxLogFileSize}"/>
            </Policies>
            <DefaultRolloverStrategy max="${rolloverCount}"/>
        </RollingRandomAccessFile>
        <RollingRandomAccessFile name="FileErr" fileName="${logLocation}/${filename}_err.log" filePattern="${logLocation}/${filename}_err-%d{MM-dd-yyyy}-%i.log" immediateFlush="false">
            <PatternLayout pattern="[^%d^] [^%-5level^] [^%t^] - [^%msg^]%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy />
                <SizeBasedTriggeringPolicy size="${maxLogFileSize}"/>
            </Policies>
            <DefaultRolloverStrategy max="${rolloverCount}"/>
        </RollingRandomAccessFile>
        <RollingRandomAccessFile name="FileMetrics" fileName="${logLocation}/${filename}_metrics.log" filePattern="${logLocation}/${filename}_metrics-%d{MM-dd-yyyy}-%i.log" immediateFlush="false">
            <PatternLayout pattern="%msg%n"/>
            <Policies>
                <SizeBasedTriggeringPolicy size="${maxLogFileSize}"/>
            </Policies>
            <DefaultRolloverStrategy max="${rolloverCount}"/>
        </RollingRandomAccessFile>
        <AppMessageAppender name="Message" directory="${logLocation}/messages"/>
    </Appenders>
</Configuration>
  1. The Custom logger codes are given below.
    package com.application.logging;

    import java.net.URL;

    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;

    public final class Log4j2Logger {

        private static final String DEFAULT_CATEGORY = "com.application";

        private Logger log;

        static {
            configureLogger();
        }

        public Logger(String category){
            this.category = category;
        }

        /**
         * Constructs Log4jLogger with default ("com.application") category
         */
        public Log4j2Logger(){
            log = LogManager.getLogger(DEFAULT_CATEGORY);
        }

        /**
         * Constructs Log4jLogger with given category
         * @param category
         */
        public Log4j2Logger(String category){
            super(category);
            log = LogManager.getLogger(category);
        }   
        /**
         * Constructs Log4jLogger with given class
         * @param clazz
         */
        public Log4j2Logger(Class clazz){
            super(clazz);
            log = LogManager.getLogger(clazz.getName());
        }

        /**
         * Log message at debug level
         * @param message
         */
        public void debug(Object message){
            log.debug(message);
        }

        /**
         * Log exception with given message at debug level
         * @param message
         * @param t
         */
        public void debug(Object message, Throwable t){
            log.debug(message,t);
        }

        /**
         * Log message at info level
         * @param message
         */
        public void info(Object message){
            log.info(message);
        }

        /**
         * Log exception with given message at info level
         * @param message
         * @param t
         */
        public void info(Object message, Throwable t){
            log.info(message,t);
        }

        /**
         * Log message at warn level
         * @param message
         */
        public void warn(Object message){
            log.warn(message);
        }

        /**
         * Log exception with given message at warn level
         * @param message
         * @param t
         */
        public void warn(Object message, Throwable t){
            log.warn(message,t);
        }

        /**
         * Log message at error level
         * @param message
         */
        public void error(Object message){
            log.error(message);
        }

        /**
         * Log exception with given message at error level
         * @param message
         * @param t
         */
        public void error(Object message, Throwable t){
            log.error(message,t);
        }

        /**
         * Log message at fatal level
         * @param message
         */
        public void fatal(Object message){
            log.fatal(message);
        }

        /**
         * Log exception with given message at fatal level
         * @param message
         * @param t
         */
        public void fatal(Object message, Throwable t){
            log.fatal(message,t);
        }

        @Override
        public void debug(String message) {
            log.debug(message);
        }

        @Override
        public void debug(String message, Throwable t) {
            log.debug(message,t);
        }

        @Override
        public void error(String message) {
            log.error(message);
        }

        @Override
        public void error(String message, Throwable t) {
            log.error(message,t);
        }

        @Override
        public void fatal(String message) {
            log.fatal(message);     
        }

        @Override
        public void fatal(String message, Throwable t) {
            log.fatal(message,t);
        }

        @Override
        public void info(String message) {
            log.info(message);
        }

        @Override
        public void info(String message, Throwable t) {
            log.info(message,t);
        }

        @Override
        public void warn(String message) {
            log.warn(message);
        }

        @Override
        public void warn(String message, Throwable t) {
            log.warn(message,t);
        }   

        @Override
        public boolean isTraceEnabled() {
            return log.isTraceEnabled();
        }

        @Override
        public void trace(Object message) {
            log.trace(message);     
        }

        @Override
        public void trace(Object message, Throwable t) {
            log.trace(message,t);
        }

        @Override
        public void trace(String message) {
            log.trace(message);
        }

        @Override
        public void trace(String message, Throwable t) {
            log.trace(message,t);
        }

        /**
         * @return true if debug level log is enabled
         */
        public boolean isDebugEnabled(){
            return log.isDebugEnabled();
        }


        public boolean isInfoEnabled(){
            return log.isInfoEnabled();
        }

        public boolean isEnableForLevel(com.application.Level level){
            return log.isEnabled(((Log4j2Level)level).getLog4jLevel());
        }

        public com.application.loggging.Level getLogLevel(){
            return Log4j2Level.convert2Log4j(log.getLevel());

        }

        public void setLogLevel(com.application.Level level){
            ((org.apache.logging.log4j.core.Logger)log).setLevel(((Log4j2Level)level).getLog4jLevel());
        }

        private static void configureLogger(){
            System.setProperty("Log4jContextSelector","org.apache.logging.log4j.core.async.AsyncLoggerContextSelector");
            System.setProperty("AsyncLogger.ThreadNameStrategy","UNCACHED");
            mbean.setSystemProperties();
            try{
                URL url = Thread.currentThread().getContextClassLoader().getResource("log4j2.xml");
                if(url != null){
                    System.setProperty("log4j.configurationFile",url.toString());
                }else{
                    System.err.println("No log4j2.xml configuration found in the classpath logger");
                }
                Log4j2Logger logger = new Log4j2Logger();
                logger.info("Log4j2 logger configured");
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
  1. Application AppMessageAppender listens to a JMS queue and logs the messages.
  package com.application.logging;

   import java.io.BufferedWriter;
   import java.io.File;
   import java.io.FileNotFoundException;
   import java.io.FileWriter;
   import java.io.IOException;
   import java.io.Serializable;
   import java.io.Writer;
   import java.util.Date;`
   import java.util.Properties;
   import java.util.concurrent.atomic.AtomicLong;

   import org.apache.commons.lang.time.FastDateFormat;
   import org.apache.log4j.helpers.LogLog;
   import org.apache.logging.log4j.core.Filter;
   import org.apache.logging.log4j.core.Layout;
   import org.apache.logging.log4j.core.LogEvent;
   import org.apache.logging.log4j.core.appender.AbstractAppender;
   import org.apache.logging.log4j.core.config.Configuration;
   import org.apache.logging.log4j.core.config.plugins.Plugin;
   import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
   import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
   import org.apache.logging.log4j.core.config.plugins.PluginElement;
   import org.apache.logging.log4j.core.config.plugins.PluginFactory;
   import org.apache.logging.log4j.core.layout.PatternLayout;
   import org.slf4j.Logger;
   import org.slf4j.LoggerFactory;

   import com.application.logging.CustomMessage;
   import com.application.framework.utils.PropertiesCache;
   import com.application.utils.xml.SpringBeanFactory;

   @Plugin(name = "AppMessageAppender", category = "Core", elementType = "appender", printObject = true)
   public final class AppMessageAppender extends AbstractAppender {

       private static final long serialVersionUID = 1L;
       private static final String DEFAULT_DATE_FORMAT = "dd_MM_yyyy/HH";
       private static final String DEFAULT_TIME_FORMAT = "HHmmss_SSS";

       private static Logger logger = LoggerFactory.getLogger(AppMessageAppender.class);
       private String directoryName;
       private String dirTimeFormat = DEFAULT_DATE_FORMAT;
       private FastDateFormat dateFormatter;
       private static FastDateFormat timeFormatter = FastDateFormat.getInstance(DEFAULT_TIME_FORMAT);
       private AtomicLong sequenceNo = new AtomicLong();

       protected AppMessageAppender(String name, Filter filter, Layout<? extends Serializable> layout,
               String directoryName, String subDirectoryDateFormat) {
           super(name, filter, layout);
           this.directoryName = directoryName;
           if (subDirectoryDateFormat != null)
               this.dirTimeFormat = subDirectoryDateFormat;
           dateFormatter = FastDateFormat.getInstance(dirTimeFormat);
       }

       @Override
       public void append(LogEvent event) {
           Writer writer = null;
           try {
               CustomMessage logMessage = (CustomMessage) event.getMessage().getParameters()[0];
               StringBuilder sb = new StringBuilder();
               if (directoryName != null) {
                   sb.append(directoryName).append("/");
               }
               Date d = new Date(event.getTimeMillis());
               sb.append(dateFormatter.format(d)).append("/").append(logMessage.getChannel());
               String dir = sb.toString();
               String processMessageId = logMessage.getMessageId();
               sb.append("/Msg_").append(logMessage.getMessageId().replaceAll("/", "")).append("_")
                       .append(timeFormatter.format(d)).append("_").append(sequenceNo.getAndIncrement())
                       .append(".log");
               String absFile = sb.toString();
               writer = writeToFile(logMessage, dir, absFile);
               logger.info("AppMessageAppender message logged: {} ", absFile);
               String channelType = logMessage.getChannel();
               Properties auditPropConfig = PropertiesCache.getAuditChannelCapability();
               String beanName = auditPropConfig.getProperty(channelType);
           } catch (IOException e) {
               LogLog.error("Exception in writing to file", e);
           } finally {
               if (writer != null) {
                   try {
                       writer.close();
                   } catch (IOException e) {
                       logger.error("Error in closing the stream");
                   }
               }
           }
       }

       private Writer writeToFile(CustomMessage logMessage, String dir, String absFile) throws IOException {
           Writer writer;
           try {
               writer = new BufferedWriter(new FileWriter(absFile));
           } catch (FileNotFoundException fne) {
               File f = new File(dir);
               f.mkdirs();
               writer = new BufferedWriter(new FileWriter(absFile));
           }
           logMessage.write(writer);
           writer.flush();
           return writer;
       }

       @PluginFactory
       public static AppMessageAppender createAppender(@PluginAttribute("name") String name,
               @PluginElement("Filter")
               final Filter filter, @PluginElement("Layout") Layout<? extends Serializable> layout,
               @PluginAttribute("directory")
               final String directoryName, @PluginAttribute("subDirectoryDateFormat")
               final String subDirectoryDateFormat, @PluginConfiguration
               final Configuration config) {
           if (layout == null) {
               layout = PatternLayout.createDefaultLayout();
           }
           return new AppMessageAppender(name, filter, layout, directoryName, subDirectoryDateFormat);
       }

   }
  1. We have a CustomMessage class also.
    package com.application.logging;

    import java.io.IOException;
    import java.io.Serializable;
    import java.io.Writer;
    import java.util.LinkedHashMap;
    import java.util.Map;

    public class CustomMessage implements Serializable{

        private static final String HEADER_SEP = ": ";
        private static final String NEWLINE = "\r\n";

        private static final String CHANNEL_HEADER_NAME = "Channel";
        private static final String MESSAGEID_HEADER_NAME = "MessageId";    
        private static final String DEFAULT_CHANNEL = "DefaultChannel";
        private static final String DEFAULT_MESSAGEID = "Message";  

        protected Map<String,String> headers = new LinkedHashMap<String,String>(4); 
        protected String body;

        public CustomMessage(String channel, String messageId){
            addHeader(CHANNEL_HEADER_NAME,channel != null ? channel : DEFAULT_CHANNEL);
            addHeader(MESSAGEID_HEADER_NAME,messageId != null ? messageId : DEFAULT_MESSAGEID);
        }

        public String getChannel(){
            return headers.get(CHANNEL_HEADER_NAME);
        }

        public String getMessageId(){
            return headers.get(MESSAGEID_HEADER_NAME);
        }

        public void addHeader(String name, String value){
            headers.put(name, value);
        }

        public void setBody(String body){
            this.body = body;
        }

        public void write(Writer w) throws IOException{
            writeHeaders(w);
            writeLine(w);
            writeBody(w);
        }

        private void writeHeaders(Writer w) throws IOException {
            if(headers != null){
                for(Map.Entry<String,String> entry : headers.entrySet()){
                    w.append(entry.getKey()).append(HEADER_SEP).append(entry.getValue());
                    writeLine(w);
                }
            }
        }

        private void writeBody(Writer w) throws IOException {
            if(body != null)
                w.write(body);
        }

        private void writeLine(Writer w) throws IOException{
            w.write(NEWLINE);
        }

        public String toString(){
            StringBuilder sb = new StringBuilder();
            if(headers != null){
                for(Map.Entry<String,String> entry : headers.entrySet()){
                    sb.append(entry.getKey()).append(HEADER_SEP).append(entry.getValue());
                    sb.append(NEWLINE);
                }
            }
            sb.append(NEWLINE);
            sb.append(body);
            return sb.toString();
        }
    }
  1. When we looked at the thread dump during the issue we saw a lot of threads with the blow stacktrace.
    "[STUCK] ExecuteThread: '33' for queue: 'weblogic.kernel.Default (self-tuning)'" #91 daemon prio=1 os_prio=0 tid=0x00007fe644022800 nid=0x3928 runnable [0x00007fe5b6ceb000]
       java.lang.Thread.State: TIMED_WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:338)
        at com.lmax.disruptor.MultiProducerSequencer.next(MultiProducerSequencer.java:136)
        at com.lmax.disruptor.MultiProducerSequencer.next(MultiProducerSequencer.java:105)
        at com.lmax.disruptor.RingBuffer.publishEvent(RingBuffer.java:465)
        at com.lmax.disruptor.dsl.Disruptor.publishEvent(Disruptor.java:331)
        at org.apache.logging.log4j.core.async.AsyncLoggerDisruptor.enqueueLogMessageInfo(AsyncLoggerDisruptor.java:222)
        at org.apache.logging.log4j.core.async.AsyncLogger.handleRingBufferFull(AsyncLogger.java:181)
        at org.apache.logging.log4j.core.async.AsyncLogger.publish(AsyncLogger.java:166)
        at org.apache.logging.log4j.core.async.AsyncLogger.logWithThreadLocalTranslator(AsyncLogger.java:161)
        at org.apache.logging.log4j.core.async.AsyncLogger.logMessage(AsyncLogger.java:131)
        at org.apache.logging.log4j.spi.AbstractLogger.tryLogMessage(AbstractLogger.java:2170)
        at org.apache.logging.log4j.spi.AbstractLogger.logMessageTrackRecursion(AbstractLogger.java:2125)
        at org.apache.logging.log4j.spi.AbstractLogger.logMessageSafely(AbstractLogger.java:2108)
        at org.apache.logging.log4j.spi.AbstractLogger.logMessage(AbstractLogger.java:2002)
        at org.apache.logging.log4j.spi.AbstractLogger.logIfEnabled(AbstractLogger.java:1974)
        at org.apache.logging.log4j.spi.AbstractLogger.debug(AbstractLogger.java:318)
        at com.application.logging.log4j.Log4j2Logger.debug(Log4j2Logger.java:129)
        at com.application.services.process.ejb.EventSubscriberBean.onMessage(EventSubscriberBean.java:52)
        at weblogic.ejb.container.internal.MDListener.execute(MDListener.java:451)
        at weblogic.ejb.container.internal.MDListener.transactionalOnMessage(MDListener.java:375)
        at weblogic.ejb.container.internal.MDListener.onMessage(MDListener.java:310)
        at weblogic.jms.client.JMSSession.onMessage(JMSSession.java:4855)
        at weblogic.jms.client.JMSSession.execute(JMSSession.java:4529)
        at weblogic.jms.client.JMSSession.executeMessage(JMSSession.java:3976)
        at weblogic.jms.client.JMSSession.access$000(JMSSession.java:120)
        at weblogic.jms.client.JMSSession$UseForRunnable.run(JMSSession.java:5375)
        at weblogic.work.SelfTuningWorkManagerImpl$WorkAdapterImpl.run(SelfTuningWorkManagerImpl.java:553)
        at weblogic.work.ExecuteThread.execute(ExecuteThread.java:311)
        at weblogic.work.ExecuteThread.run(ExecuteThread.java:263)

Questions:

  1. Is this an issue with the load on log4j2 and the disruptor, not able to handle ?
  2. Apart from increasing the ringBuffersize, what other optimizations can be done ?
  3. we are using log4j2 2.11.0 and disruptor 3.4.1 version, Running it in OEL 3.8.13-98.el6uek.x86_64, is there any known issue for these combinations ?

Happy to clarify anything more asked.

Quick Edit -: We have seen this over a period of time. Initially the logs are in sync with the server time stamp i.e. logs match the server timestamp. As time progresses, the logging starts slowing down and we see logs which are 3 hours earlier (sometime it goes to 8 hours) getting printed in the logs. I hope that rings a bell somewhere.

Thanks


Solution

  • I suspect that your application is producing log events faster than the Appenders can handle. The configuration shows two ConsoleAppenders. Logging to the console is extremely slow, literally 50x slower than logging to a file.

    Async Loggers use a ringbuffer. If this buffer is full (because the Appender cannot take out events fast enough), then the application will not be able to add new events to the queue, so effectively the application is blocked from making progress.

    My advice is to only log WARN or ERROR events to the Console, and everything else to files.

    Another potential bottleneck is the custom Appender. Note that it opens a new FileWriter for every event. This is going to be slow. Better to open the file once and keep appending to it.