Groovy cli app with specifyable log4j target logfile

It may happen, that you wish to specify a custom location of your logfile when running your Java app. Within this post I use an example cli application that is written in Groovy and build with Gradle. By passing „–logfile=mylogfile.log“ it will write all logs to the specified location, while it is using the Slf4j-annotation described in one of my previous posts.

Environment:

  • Java: Hotspot, 1.8.0_92
  • Gradle: 2.14
  • Groovy: 2.4.4

The project structure:

.
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── settings.gradle
└── src
    └── main
        ├── groovy
        │   └── eu
        │       └── christophburmeister
        │           └── playground
        │               ├── logic
        │               │   └── MyLogicClass.groovy
        │               └── Main.groovy
        └── resources
            └── log4j.xml

The stuff we want to log is located in „MyLogicClass“:

package eu.christophburmeister.playground.logic

import groovy.util.logging.Slf4j

@Slf4j
class MyLogicClass {

    public doSth(String[] args){
        log.info 'entering doSth method'
        // ...
        log.info 'leaving doSth method'
    }

}

As you can see, the Slf4j annotation is used here. Together with the log4j-dependencies in build.gradle the applicaton will use log4j configuration found on classpath at runtime:

group 'eu.christophburmeister.playground'
version '0.1.0'

apply plugin: 'groovy'
apply plugin: 'application' // contains task installDist

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.codehaus.groovy:groovy-all:2.3.11'
    compile 'org.slf4j:slf4j-api:1.7.25'

    runtime 'org.apache.logging.log4j:log4j-core:2.8.2'
    runtime 'org.slf4j:slf4j-log4j12:1.7.25'
}

installDist {
    mainClassName = 'eu.christophburmeister.playground.Main'
}
rootProject.name = 'groovy-gradle-cli'

In the provided log4j.xml there is a placeholder property in the file appender for logfilename:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration debug="false" xmlns:log4j='http://jakarta.apache.org/log4j/'>

    <appender name="my-file-appender" class="org.apache.log4j.FileAppender">
        <param name="file" value="${logfilename}"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"/>
        </layout>
    </appender>
    <appender name="my-console-appender" class="org.apache.log4j.ConsoleAppender">
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"/>
        </layout>
    </appender>

    <logger name="eu.christophburmeister.playground" additivity="false">
        <level value="info"/>
        <appender-ref ref="my-file-appender"/>
    </logger>

    <root>
        <level value="ERROR"/>
        <appender-ref ref="my-console-appender"/>
    </root>

</log4j:configuration>

This property can be obtained from a System property or it can be set as Java argument (-Dlogfilename=…)

Using this with a raw „java -jar“ call is no problem: The JVM will start with the parameter set and while construction of log4j-configuration it is used.
But I might need to pass it as program argument „–logfilename“, so I need a bit of program logic in my Main class:

package eu.christophburmeister.playground

import eu.christophburmeister.playground.logic.ParamWriter

// @Slf4j cannot be used here as the system parameter "logfilename" is not yet set,
// but required for initialization of log4j.xml
class Main {

    private static final String DEAULT_LOGFILENAME = 'app.log'

    public static void main(String[] args) {
        if (args.size() == 0) {
            // println the usage notes
        } else {
            setLogfilename(args)
            // now after setting the value for logfile's name,
            // the ParamWriter class can get injected with groovy's slf4j annotation
            // because now the log4 config can be constructed without NPE
            ParamWriter p = new ParamWriter()
            p.writeParams(args)
        }
    }

    /**
     * if a param '--logfilename=xxx' is given, it will set the logfile accordingly
     * otherwise the default logfile 'app.log' will be used.
     *
     * background:
     * The logfile name will be set as system property in order to be used while initialization
     * of log4j (or logback) configuration
     *
     * <log4j:configuration>
     *     ...
     *          <appender name="my-file-appender" class="org.apache.log4j.FileAppender">
     *              <param name="file" value="${logfilename}"/>
     *                  ...
     *          </appender>
     *     ...
     * </log4j:configuration>
     *
     */
    private static void setLogfilename(def args) {
        String logfilename = DEAULT_LOGFILENAME
        // set default logfile
        System.setProperty('logfilename', 'app.log')
        // look for an alternative in args
        args.each { arg ->
            if (arg.contains('--logfilename')) {
                logfilename =  arg.split('=')[1]
            }
        }
        System.setProperty('logfilename', logfilename)
        println "app is now logging to '${logfilename}'"
    }
}

But take care: If you want to use Slf4j annotation in Main class, you will be confronted with a FileNotFound-exception because the System property is set after the initializatoin of log4j configuration would have been done and that is simply not possible.

build the app:

./gradlew clean installDist

run the app:

./build/install/groovy-gradle-cli/bin/groovy-gradle-cli 

run the app with parameters:

./build/install/groovy-gradle-cli/bin/groovy-gradle-cli anakin padme

run the app with parameters and a custom logfile:

./build/install/groovy-gradle-cli/bin/groovy-gradle-cli anakin padme --logfilename=mylog.app