Joe Joe - 4 months ago 66
Java Question

Log4j2 RollingFile Appender - add custom info at the start of each logfile

Using log4j2 (beta9) with java 1.7.

My complete log4j2.xml:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Properties>
<Property name="projectPrefix">Tts</Property>
<Property name="rawPattern">%d %-5p [%t] %C{2} (%F:%L) - %m%n</Property>
<Property name="coloredPattern">%d %highlight{%-5p}{FATAL=bright red, ERROR=red, WARN=yellow, INFO=cyan, DEBUG=green, TRACE=bright blue} %style{[%t] %C{2} (%F:%L) -}{bright,black} %m%n</Property>
<Property name="fileName">Log/${projectPrefix}.log</Property>
<Property name="filePattern">Log/${projectPrefix}-%i.log</Property>
</Properties>
<Appenders>
<Console name="Stdout" target="SYSTEM_OUT">
<PatternLayout pattern="${coloredPattern}"/>
</Console>
<RollingFile name="Logfile" fileName="${fileName}" filePattern="${filePattern}">
<PatternLayout pattern="${rawPattern}"/>
<Policies>
<SizeBasedTriggeringPolicy size="16 MB"/>
</Policies>
<DefaultRolloverStrategy fileIndex="min" max="16"/>
</RollingFile>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Stdout"/>
<AppenderRef ref="Logfile"/>
</Root>
</Loggers>
</Configuration>


I want to add some custom info at the top of each logfile, like the version string of my application, the application uptime and the system uptime. And even writing some »bye, bye / eof« to the bottom of the just closed logfile would also be fine.

Is there something like a hook or callback to get notified when the RollingFileAppander has created a new file, so that I can put my things at first into these new logfile (or any other suggestion)?

Joe Joe
Answer

Ok, there is a working solution for this problem by extending DefaultRolloverStrategy like described here. But

  • it needs about 150 lines of code (incl. wrapping RolloverDescription and appender.rolling.helper.Action) and
  • smells a bit because the need to completely copy DefaultRolloverStrategy's factory method (makes this solution maintenance-unfriendly eg. if DefaultRolloverStrategy gets more config parameters in future versions)

To let log4j2 call our factory method, the root tag of log4j2.xml must be attributed with the package of our class, e.g.:

<Configuration packages="de.jme.toolbox.logging">
...
</Configuration>

and within our own RolloverStrategy we have to deal with @Plugin and @PluginFactory as described here.

Finally here my complete log4j2.xml (you don't need all that properties - that's just the way how I like to configure my logging):

<?xml version="1.0" encoding="UTF-8"?>
<Configuration packages="de.jme.toolbox.logging">
  <Properties>
    <Property name="projectPrefix">Tts</Property>
    <Property name="rawPattern">%d %-5p [%t] %C{2} (%F:%L) - %m%n</Property>
    <Property name="coloredPattern">%d %highlight{%-5p}{FATAL=bright red, ERROR=red, WARN=yellow, INFO=cyan, DEBUG=green, TRACE=bright blue} %style{[%t] %C{2} (%F:%L) -}{bright,black} %m%n</Property>
    <Property name="coloredShortPattern">%d %highlight{%-5p}{FATAL=bright red, ERROR=red, WARN=yellow, INFO=cyan, DEBUG=green, TRACE=bright blue} %style{[%t] -}{bright,black} %m%n</Property>
    <Property name="fileName">Log/${projectPrefix}.log</Property>
    <Property name="filePattern">Log/${projectPrefix}-%i.log</Property>
  </Properties>
  <Appenders>
    <Console name="Stdout" target="SYSTEM_OUT">
      <PatternLayout pattern="${coloredPattern}"/>
    </Console>
    <RollingFile name="Logfile" fileName="${fileName}" filePattern="${filePattern}">
      <PatternLayout pattern="${rawPattern}"/>
      <Policies>
        <SizeBasedTriggeringPolicy size="1 MB"/>
      </Policies>
      <MyRolloverStrategy fileIndex="min" max="16"/>
    </RollingFile>
  </Appenders>
  <Loggers>
    <Root level="info">
      <AppenderRef ref="Stdout"/>
      <AppenderRef ref="Logfile"/>
    </Root>
  </Loggers>
</Configuration>

And here MyRolloverStrategy.java:

package de.jme.toolbox.logging;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.zip.Deflater;

import org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy;
import org.apache.logging.log4j.core.appender.rolling.RollingFileManager;
import org.apache.logging.log4j.core.appender.rolling.RolloverDescription;
import org.apache.logging.log4j.core.appender.rolling.helper.Action;
import org.apache.logging.log4j.core.config.Configuration;
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.PluginFactory;
import org.apache.logging.log4j.core.helpers.Integers;
import org.apache.logging.log4j.core.lookup.StrSubstitutor;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.status.StatusLogger;

/**
 * Own RolloverStrategy to hook the DefaultRolloverStrategy's rollover events
 *
 * Siehe auch:
 * - https://issues.apache.org/jira/browse/LOG4J2-486
 * - http://apache-logging.6191.n7.nabble.com/log4j2-getting-started-amp-rolling-files-tt8406.html#a42402
 * - http://stackoverflow.com/questions/20819376/log4j2-rollingfile-appender-add-custom-info-at-the-start-of-each-logfile
 *
 * @author Joe Merten
 */
@org.apache.logging.log4j.core.config.plugins.Plugin(name="MyRolloverStrategy", category="Core", printObject=true)
public class MyRolloverStrategy extends DefaultRolloverStrategy {

    protected static final Logger logger = StatusLogger.getLogger();

    // ==============================
    // ↓↓↓ Some stuff copied from ↓↓↓
    // https://svn.apache.org/repos/asf/logging/log4j/log4j2/trunk/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/DefaultRolloverStrategy.java r1556050
    // Just changed »DefaultRolloverStrategy« to »MyRolloverStrategy«

    private static final int MIN_WINDOW_SIZE = 1;
    private static final int DEFAULT_WINDOW_SIZE = 7;

    @PluginFactory
    public static MyRolloverStrategy createStrategy(
            @PluginAttribute("max") final String max,
            @PluginAttribute("min") final String min,
            @PluginAttribute("fileIndex") final String fileIndex,
            @PluginAttribute("compressionLevel") final String compressionLevelStr,
            @PluginConfiguration final Configuration config) {
        final boolean useMax = fileIndex == null ? true : fileIndex.equalsIgnoreCase("max");
        int minIndex;
        if (min != null) {
            minIndex = Integer.parseInt(min);
            if (minIndex < 1) {
                LOGGER.error("Minimum window size too small. Limited to " + MIN_WINDOW_SIZE);
                minIndex = MIN_WINDOW_SIZE;
            }
        } else {
            minIndex = MIN_WINDOW_SIZE;
        }
        int maxIndex;
        if (max != null) {
            maxIndex = Integer.parseInt(max);
            if (maxIndex < minIndex) {
                maxIndex = minIndex < DEFAULT_WINDOW_SIZE ? DEFAULT_WINDOW_SIZE : minIndex;
                LOGGER.error("Maximum window size must be greater than the minimum windows size. Set to " + maxIndex);
            }
        } else {
            maxIndex = DEFAULT_WINDOW_SIZE;
        }
        final int compressionLevel = Integers.parseInt(compressionLevelStr, Deflater.DEFAULT_COMPRESSION);
        return new MyRolloverStrategy(minIndex, maxIndex, useMax, compressionLevel, config.getStrSubstitutor());
    }
    // ↑↑↑ Some stuff copied from ↑↑↑
    // ==============================

    protected MyRolloverStrategy(int minIndex, int maxIndex, boolean useMax, int compressionLevel, StrSubstitutor subst) {
        super(minIndex, maxIndex, useMax, compressionLevel, subst);
    }

    // Wrapper class only for setting a hook to execute()
    static class MyAction implements Action {
        final Action delegate;
        final String fileName;

        public MyAction(final Action delegate, final String fileName) {
            this.delegate = delegate;
            this.fileName = fileName;
        }

        @Override public void run() {
            delegate.run();
        }

        @Override public boolean execute() throws IOException {
            try {
                BufferedWriter writer = null;
                try {
                    writer = new BufferedWriter(new FileWriter(new File(fileName), true));
                    writer.write("****************************\n");
                    writer.write("*** Bye, bye old logfile ***\n");
                    writer.write("****************************\n");
                } finally {
                    if (writer != null)
                        writer.close();
                }
            } catch (Throwable e) {
                logger.error("Writing to bottom of old logfile \"" + fileName + "\" with", e);
            }

            boolean ret = delegate.execute();

            try {
                BufferedWriter writer = null;
                try {
                    writer = new BufferedWriter(new FileWriter(new File(fileName), true));
                    writer.write("*************************\n");
                    writer.write("*** Hello new logfile ***\n");
                    writer.write("*************************\n");
                } finally {
                    if (writer != null)
                        writer.close();
                }
            } catch (Throwable e) {
                logger.error("Writing to top of new logfile \"" + fileName + "\" with", e);
            }

            return ret;
        }

        @Override public void close() {
            delegate.close();
        }

        @Override public boolean isComplete() {
            return delegate.isComplete();
        }
    }

    // Wrapper class only for setting a hook to getSynchronous().execute()
    static class MyRolloverDescription implements RolloverDescription {
        final RolloverDescription delegate;

        public MyRolloverDescription(final RolloverDescription delegate) {
            this.delegate = delegate;
        }

        @Override public String getActiveFileName() {
            return delegate.getActiveFileName();
        }

        @Override public boolean getAppend() {
            //return delegate.getAppend();
            // As long as we already put some data to the top of the new logfile, subsequent writes should be performed with "append".
            return true;
        }

        // The synchronous action is for renaming, here we want to hook
        @Override public Action getSynchronous() {
            Action delegateAction = delegate.getSynchronous();
            if (delegateAction == null) return null;
            return new MyAction(delegateAction, delegate.getActiveFileName());
        }

        // The asynchronous action is for compressing, we don't need to hook here
        @Override public Action getAsynchronous() {
            return delegate.getAsynchronous();
        }
    }

    public RolloverDescription rollover(final RollingFileManager manager) {
        RolloverDescription ret = super.rollover(manager);
        return new MyRolloverDescription(ret);
    }
}

Solving this requirement might be easier in future versions of log4j2, if my posted feature request would implemented.