Abdull
Abdull

Reputation: 27822

Java command line tool, verbose output flag

I am developing a Java-based command-line tool. I want to leverage slf4j's logging abstraction.

When the application is regularly invoked

java -jar myapp.jar someparameter

then the application shall give "plain" CLI output on stdout such as:

/some/file1
/some/file2


When instead the application is invoked with an additional --verbose flag / option

java -jar myapp.jar --verbose someparameter

then the application shall give more advanced logging:

16:06:09.031 [main] DEBUG com.example.MyApp - Starting application.
16:06:09.031 [main] DEBUG com.example.MyApp - Entering directory /some/.
16:06:09.046 [main] INFO  com.example.MyApp - /some/file1
16:06:09.046 [main] INFO  com.example.MyApp - /some/file2


While it is is easy to determine whether --verbose was provided (e.g. by using the jCommander CLI library), slf4j does not seem to allow setting the root logger level during runtime, nor does slf4j allow to change the log entry pattern layout during runtime.

Upvotes: 2

Views: 20519

Answers (3)

Abdull
Abdull

Reputation: 27822

I came up with the following solution:

For the executable Java application side of the command-line tool, I decide to use slf4j and logback. I bundle the application with two logback configuration files, logback.xml and logback-verbose.xml.

logback.xml:

<configuration>

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%msg%n</pattern>
    </encoder>
  </appender>

  <root level="info">
    <appender-ref ref="STDOUT" />
  </root>

  <logger name="com.example.some.chatty.library.i.want.to.turn.off.logging" level="OFF">
    <appender-ref ref="STDOUT" />
  </logger>

</configuration>

logback-verbose.xml:

<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%date{ISO8601} [%-17thread] %-5level %-36logger - %msg%n</pattern>
    </encoder>
  </appender>

  <root level="debug">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

Early in the application start-up phase, I check for the --verbose flag. If that flag is set, I command logback to use configuration file logback-verbose.xml instead of the default logback.xml:

Application.java:

package com.example.my;
//

public class Application {

    public static void main(String[] args) throws Exception {

        // reconfigure JUL for slf4j
        LogManager.getLogManager().reset();
        SLF4JBridgeHandler.removeHandlersForRootLogger();
        SLF4JBridgeHandler.install();
        Logger.getLogger("global").setLevel(Level.FINEST);

        // about to check for verbose output request
        if ( isVerboseOutputRequested() ) {
            LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();

            try {
              JoranConfigurator configurator = new JoranConfigurator();
              configurator.setContext(context);
              // Call context.reset() to clear any previous configuration, e.g. default 
              // configuration. For multi-step configuration, omit calling context.reset().
              context.reset(); 
              configurator.doConfigure( getClass().getResourceAsStream("/logback-verbose.xml") );
            }
            catch (JoranException je) {
              // StatusPrinter will handle this
            }
            StatusPrinter.printInCaseOfErrorsOrWarnings(context);
        }

       startApplication(); 
    }

    // ... implement isVerboseOutputRequested() and startApplication()
}

Rationale

This setup keeps in mind a separation of an executable Java application apart from a library used by that application.

The library can depend on slf4j only, while the executable Java application (being just a thin wrapper around that library) depends on both slf4j and logback in order to provide an actual logging implementation for the final executable artifact.

Thanks to Carlitos Way and Tansir1 for providing answers that lead to this solution.

Upvotes: 4

Carlitos Way
Carlitos Way

Reputation: 3424

I suggest to use a "decorator" pattern for this problem.

You will have to define your own LoggerFactory, and define the method "getLogger"... something like this:

public class LoggerFactory {
    public static Logger getLogger(String category) {
        return (Boolean.parseBoolean(System.getProperty("verboseFlag"))
                  ? org.slf4j.LoggerFactory.getLogger("verbose-" + category)
                  : org.slf4j.LoggerFactory.getLogger(category);
    }

Then in your configuration file, assuming that your are using log4j, you must configure two category; one with the verbose prefix another without it... later, each category must define its own appenders and each appender must be configured accordingly...

<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
        <appender name="VerboseConsoleAppender" class="org.apache.log4j.ConsoleAppender">
    <layout class="org.apache.log4j.PatternLayout">
        <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] %m%n"/>
    </layout>
</appender>

        <appender name="RegularConsoleAppender" class="org.apache.log4j.ConsoleAppender">
    <layout class="org.apache.log4j.PatternLayout">
        <param name="ConversionPattern" value="%m%n"/>
    </layout>
</appender>
    <category name="verbose-org.xyz" additivity="false">
        <priority value="DEBUG" />
        <appender-ref ref="VerboseConsoleAppender" />
    </category>

    <category name="org.xyz" additivity="false">
        <priority value="INFO" />
        <appender-ref ref="RegularConsoleAppender" />
    </category>
</log4j:configuration>

Upvotes: 1

Tansir1
Tansir1

Reputation: 1819

Changing the logging configuration dynamically at runtime is not part of SLF4J. Logging configuration is specific to the abstracted logging mechanism (log4j, logback, etc).

Assuming you're using the Logback library underneath SLF4J you can reconfigure everything programatically by changing the JoranConfigurator. This code snippet was copied from Invoking JoranConfigurator directly in the Logback documentation website. You can change the logging pattern, appenders, logging level, and just about every other parameter about the loggers from the JoranConfigurator.

package chapters.configuration;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.util.StatusPrinter;

public class MyApp3 {
  final static Logger logger = LoggerFactory.getLogger(MyApp3.class);

  public static void main(String[] args) {
    // assume SLF4J is bound to logback in the current environment
    LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();

    try {
      JoranConfigurator configurator = new JoranConfigurator();
      configurator.setContext(context);
      // Call context.reset() to clear any previous configuration, e.g. default 
      // configuration. For multi-step configuration, omit calling context.reset().
      context.reset(); 
      configurator.doConfigure(args[0]);
    } catch (JoranException je) {
      // StatusPrinter will handle this
    }
    StatusPrinter.printInCaseOfErrorsOrWarnings(context);

    logger.info("Entering application.");

    Foo foo = new Foo();
    foo.doIt();
    logger.info("Exiting application.");
  }
}

Upvotes: 0

Related Questions