0

We are using a fairly simple log4j2.xml configuration file to log to stdout. However there are cases where we want to change this configuration programmatically after the start of the application to use a log file that is handed over on the command line.

For this I followed the suggestion on the log4j2 homepage and wrote the following method

static void divertLogging(String logFile, Level level) {
    ConfigurationBuilder<BuiltConfiguration> builder = ConfigurationBuilderFactory.newConfigurationBuilder();

    AppenderComponentBuilder appenderBuilder 
        = builder.newAppender("File", "FILE").addAttribute("fileName", logFile).addAttribute("append", "false");

    appenderBuilder.add(builder.newLayout("PatternLayout")
        .addAttribute("pattern", "%d [%t] %-5level: %msg%n%throwable"));

    builder.add(appenderBuilder);
    builder.add(builder.newRootLogger(level).add(builder.newAppenderRef("File")));

    try {
        builder.writeXmlConfiguration(System.out);
    } catch (IOException e) {
        throw new ApplicationException(e);
    }

    BuiltConfiguration configuration = builder.build();

    Configurator.initialize(configuration);
    ((LoggerContext)LogManager.getContext(false)).updateLoggers(configuration);
}

We get the following output

<?xml version="1.0" ?>
<Configuration>
    <Appenders>
        <FILE name="File" fileName="test.log" append="false">
            <PatternLayout pattern="%d [%t] %-5level: %msg%n%throwable"/>
        </FILE>
    </Appenders>
    <Loggers>
        <Root level="INFO">
            <AppenderRef ref="File"/>
        </Root>
    </Loggers>
</Configuration>

and then the log message

ERROR Attempted to append to non-started appender File

After that logging continues to be output to stdout and the desired log file stays empty.

Anyone having an idea what I am doing wrong?

D.B.
  • 4,523
  • 2
  • 19
  • 39
  • 1
    According to [this](https://stackoverflow.com/questions/39228697/log4j2-custom-appender-error-attempted-to-append-to-non-started-appender) you need to call start() – Erwin Sep 23 '19 at 08:35
  • I saw that posting while searching for a solution also. I was under the impression that log4j2 takes care of starting the appenders; especially, since I am only constructing builders and not the objects itself. However adding a line `configuration.getAppender("logFile").start();` before the last line in the above method appearantly solved the problem... – Andreas Spengler Sep 23 '19 at 08:59
  • ...but only for the next log statement in the same class. Other log statements get output to stdout again... :-( – Andreas Spengler Sep 23 '19 at 10:14

2 Answers2

1

You don't need programmatic configuration to do what you want, and I would strongly discourage you from using it since that would make your code depend on the log4j2 implementation rather than its public interface.

To change the file dynamically at runtime you can use the RoutingAppender together with a lookup. See the log4j2 FAQ page for details.

Here is a sample log4j2 configuration:

<?xml version="1.0" ?>
<Configuration>
    <Appenders>
        <Routing name="myAppender">
            <Routes pattern="$${main:0}">
                <!-- This route is chosen if there is no value for main argument 0 -->
                <Route key="$${main:0}">
                    <File
                        fileName="logs/default.log"
                        name="myAppender-default">
                        <PatternLayout pattern="%d [%t] %-5level: %msg%n%throwable"/>
                    </File>
                </Route>
                <!-- This route is chosen if there is a value for main argument 0 -->
                <Route>
                    <File
                        fileName="logs/${main:0}.log"
                        name="myAppender-${main:0}">
                        <PatternLayout pattern="%d [%t] %-5level: %msg%n%throwable"/>
                    </File>
                </Route>
            </Routes>
        </Routing>
    </Appenders>
    <Loggers>
        <Root level="INFO">
            <AppenderRef ref="myAppender"/>
        </Root>
    </Loggers>
</Configuration>

Here is some sample Java code to generate some logs:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.core.lookup.MainMapLookup;

public class SomeClass {

    private static final Logger LOG = LogManager.getLogger();   

    public static void main(String[] args){
        MainMapLookup.setMainArguments(args);

        LOG.info("This should appear in default.log");

        args = new String[]{"specialFile"};
        MainMapLookup.setMainArguments(args);
        LOG.info("This should appear in specialFile.log");
    }
}

When the above code is executed without passing a program argument, 2 logs are generated each with 1 entry. The default.log contains the first log entry and the specialFile.log contains the second. If you pass a program argument it will be used as the log file name in which case no entry would appear in default.log - as illustrated by the second log where we simulate passing a single argument by creating the new String array.

Hope this helps!

D.B.
  • 4,523
  • 2
  • 19
  • 39
0

try this

    /**
     * 
     * @param logType 0 = console, 1 = file
     * @param logFile
     * @param level
     */
    private void divertLogging(int logType, String logFile, Level level) {
        ConfigurationBuilder<BuiltConfiguration> builder = ConfigurationBuilderFactory.newConfigurationBuilder();

        AppenderComponentBuilder appenderBuilder;

        if (logType == 0)
            appenderBuilder = builder.newAppender("Stdout", "CONSOLE").addAttribute("target", ConsoleAppender.Target.SYSTEM_OUT);
        else
            appenderBuilder = builder.newAppender("File", "FILE").addAttribute("fileName", logFile).addAttribute("append", "false");

        appenderBuilder.add(builder.newLayout("PatternLayout")
            .addAttribute("pattern", "%d [%t] %-5level: %msg%n%throwable"));

        builder.add(appenderBuilder);
        if (logType == 1)
            builder.add(builder.newRootLogger(level).add(builder.newAppenderRef("File")));

        try {
            builder.writeXmlConfiguration(System.out);
        } catch (IOException e) {
            e.printStackTrace();
        }

        BuiltConfiguration configuration = builder.build();

        Configurator.initialize(configuration);
        ((LoggerContext)LogManager.getContext(false)).updateLoggers(configuration);
        if (logType == 1)
            configuration.getAppender("File").start();
    }

    public static void main(String[] args) {
        Log4j2Test test = new Log4j2Test();
        test.divertLogging(0, null, Level.ALL);
        logger.error("Log to console 1");
        test.divertLogging(1, "C:\\Java\\test\\output\\test.log", Level.ALL);
        logger.error("Log to file 2");
        test.divertLogging(0, null, Level.ALL);
        logger.error("Log to console 3");
        test.divertLogging(1, "C:\\Java\\test\\output\\test.log", Level.ALL);
        logger.error("Log to file 4");
    }

Why my answer is downvoted?

Erwin
  • 460
  • 2
  • 6
  • Apart from the xml output I see the following: `14:09:39.756 [main] ERROR LogTest - Log to console 1` `14:09:40.072 [main] ERROR LogTest - Log to console 3` In C:\Java\test\output\test.log I see `2019-09-23 14:09:40,062 [main] ERROR: Log to file 2` `2019-09-23 14:09:40,080 [main] ERROR: Log to file 4` – Andreas Spengler Sep 23 '19 at 12:11
  • yes it is the error log that I put in the main method that diverting between console/stdout and log file, just call the divertLogging method to divert it – Erwin Sep 23 '19 at 13:59
  • So do I understand this correctly: once a log4j2.xml file based configuration is active, there is no way of overwriting it programmatically? – Andreas Spengler Sep 23 '19 at 14:03
  • If you still want to use log4j2.xml for base configuration then I think you should load the file manually in the divertLogging method. Just make the logType 0 to read the file, you can refer [here](https://stackoverflow.com/a/28701154/12083015). I'll edit my answer too – Erwin Sep 23 '19 at 14:20
  • Tried it myself and put the manual loading of log4j2.xml (as shown in the link you provided) in the static initializer of my start class. Doesn't work :-( – Andreas Spengler Sep 23 '19 at 14:40
  • Appearantly the updateLoggers() call only affects existing loggers. For all other loggers the file-based configuration is used... – Andreas Spengler Sep 23 '19 at 14:44
  • Yes, it didn't work. But in my opinion I think you should configure both logging to stdout and file by programmatically. May be make the code more configurable by adding more parameters to the method. – Erwin Sep 23 '19 at 14:57
  • [Here](https://logging.apache.org/log4j/2.x/manual/customconfig.html#Hybrid) is the way to combine configuration file with programmatic. Seem work, I have tried, but I have issues in add log4j2 programmatic configuration in doConfigure – Erwin Sep 24 '19 at 06:34