Using log4j2 (2.3) with Android

The versatile log4j is a popular logging framework for Java. Log4j 2 brings new advanced features and extensibility. Unfortunately, out of the box, it’s not very Android friendly. I spent a good day trying to perfect the usage and integration. Given the difficulties and workarounds required, I would hesitate to use Log4j2 for any Android project. This blog post is part commentary, part guide. If you really want to use it, check out my log4j2-android github for an example project. Hopefully this helps somebody out there.

The latest versions of log4j require Java 7. Since Android does not have full support for Java 7 when your app compatibility is under API 19, the latest version you could use that’s Java 6 compatible is log4j 2.3. This is the version that we will use.

Add these dependencies to your app build.gradle:

[java]
dependencies {

compile ‘org.apache.logging.log4j:log4j-api:2.3’
compile ‘org.apache.logging.log4j:log4j-core:2.3’
[/java]

Reflection Everywhere!!

One the annoying things I found was that Log4j 2 used reflection heavily, especially on app start up when it tries to initialise all its plugins. The effect is that app load time is slightly longer. Reflection also means you need to put exceptions for proguard.

LogcatAppender

Log4j allows you to configure different appenders. Common appenders are the ConsoleAppender and FileAppender. ConsoleAppender uses the Java System.err and System.out to print log messages. In Android, they get routed to Logcat, but they will all be in the INFO category and have no ability to specify a tag. With log4j, you could create your own appender plugins, so I’ve defined a LogcatAppender. The appender maps logger.error to Log.e, logger.debug to Log.d, etc etc and uses the logger name as the tag.

AndroidLookup

Another type of plugin you could create is a Lookup. Lookups are custom variables which you can reference in your log config files. When specifying the file path to your log file, a lookup value that will come in handy is the android data directory. The AndroidLookup I’ve created allows you to use ${android:filesdir}, ${android:externalfilesdir} or ${android:logfilesdir} placeholder variables.

Configuration

By default log4j searches and loads its configuration from the file system using a bunch of rules. This is not ideal in an Android environment where the app is packaged an an APK. The default behaviour can be overridden by providing a custom ConfigurationSource to the LoggerContext. I’ve put the log4j XML configuration in a raw resource and implemented a custom ContextSelector to initialise the LoggerContext with the custom config. All of this is then tied together by the AndroidLog4jHelper class I’ve created.

AndroidLog4jHelper also injects plugins

There is another purpose to AndroidLog4jHelper and that is to inject plugins. As mentioned, Log4j uses reflection heavily. One of the problems is that Android/ART/Dalvik is not Java, and hence some assumptions that are made in Log4j, based on the JRE, aren’t applicable to Android. Typically, the PluginManager.addPackage method is used to register plugins. It works by finding the package folder in the .jar and enumerating the .class files. Then it would use ClassLoader and reflection to find the @Plugin annotations to register. In Android, we don’t have .class files in a .jar (or apk), hence it doesn’t find anything. To make the LogcatAppender and AndroidLookup plugins work correctly, I’ve devised a hack – Using reflection, I manually injected the plugins to a privately held Map in the PluginRegistry. It would be helpful if Log4j natively provide a method to manually register plugins by passing the class type.

Using AndroidLog4jHelper and Plugins

The key classes for this log4j android solution are in this package folder. Copy them to your project.

To use the AndroidLog4jHelper, in your Application sub class onCreate method, put:

[java]
if (BuildConfig.DEBUG) {
AndroidLog4jHelper.initialise(this.getApplicationContext(), R.raw.log4j_debug);
} else {
AndroidLog4jHelper.initialise(this.getApplicationContext(), R.raw.log4j_release);
}
[/java]

Note that we are specifying different XML configs for debug and release. If you only require one config, then you don’t need to have the if condition. Create the raw resources log4j_debug.xml and log4j_release.xml. An example XML config which uses LogcatAppender and AndroidLookup:

[xml]
<?xml version="1.0" encoding="UTF-8"?>

<Configuration status="debug">
<Appenders>
<Logcat name="Logcat">
<ThresholdFilter level="ALL" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="%m" />
</Logcat>
<RollingFile name="RollingFile" fileName="${android:logfilesdir}/app.log" filePattern="app-%i.log.gz">
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout>
<Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
</PatternLayout>
<Policies>
<SizeBasedTriggeringPolicy size="1 MB"/>
</Policies>
<DefaultRolloverStrategy max="20"/>
</RollingFile>
</Appenders>

<Loggers>
<Root level="DEBUG">
<AppenderRef ref="Logcat"/>
<AppenderRef ref="RollingFile"/>
</Root>
</Loggers>
</Configuration>
[/xml]

In the class in which you want to log, put:

[java]
private static final Logger logger = LogManager.getLogger("MyClass");
[/java]

Then you can use:

[java]
logger.info("Log4j2 succcess");
[/java]

Using the config above, you should see the log messages appear in Logcat. Messages will also be logged to the filesystem, under [your ext data directory]/logs/app.log

Proguard rules for Log4j

When releasing the APK to the public, a best practise is to utilise proguard to reduce the size of the file and obfuscate methods from sticky beaks. Since Log4j uses reflection, here are the proguard rules required for it to run properly. The presence of -keepattributes Signature may compromise code obfuscation, but unfortunately is required for log4j to work properly.

[java]
-dontwarn org.apache.logging.log4j.**
-keepattributes Signature
-keep class org.apache.logging.log4j.** { *; }
-keep class net.loune.log4j2android.** { *; }
[/java]

Exclude miscellaneous META-INF files

If you want to avoid build errors when generating an APK (You do!!!), you need to exclude some dupe’d META-INF files that are present in the log4j2 jar files. Modify your app build.gradle as such:

[java]
android {

packagingOptions {
exclude ‘META-INF/LICENSE’
exclude ‘META-INF/NOTICE’
exclude ‘META-INF/DEPENDENCIES’
}
[/java]

In conclusion, Log4j2 could be comfortably used with Android, but with some compromises. If the start up performance and proguard rules are acceptable to you, and you like the extensibility of the various L4J plugins, then Log4j2 might be the logging solution for you.

14 thoughts on “Using log4j2 (2.3) with Android

  1. Antonio Otero

    Hello.
    I’ve tried to use all of this stuff, and it doesn’t work for me.
    The very first time I run de app I get this message on logcat:

    06-06 15:13:30.261: I/dalvikvm(2880): Could not find method org.osgi.framework.FrameworkUtil.getBundle, referenced from method org.apache.logging.log4j.core.config.plugins.util.ResolverUtil.loadImplementationsInBundle

    A couple of lines below I can see this message on LogCat:
    06-06 15:13:32.611: W/System.err(2880): ERROR StatusLogger Log4j2 could not find a logging implementation. Please add log4j-core to the classpath. Using SimpleLogger to log to the console…

    And nothing is logger either on LogCat or in file.

    Do yoy have any idea or hint to be able to get this working?

    Thanks a lot beforehand

  2. Loune Post author

    @Antonio Otero, I’m not able to reproduce the issue. What devices/settings are you using? I just tried with Android Studio 2.1.2 / JDK 1.8 on Mac OS X and running it on Android 6.0 emulator (debug mode) and works fine.

  3. Antonio Otero

    Hello!
    I’m using Eclipse Luna 4.4.2, maven with simpligility android plugin on Samsung Galaxy Active with Android 4.4.4.
    The first time with Log4J 2.6, then I realised the example was made with Log4J 2.3 so I tried with this version but with the same results.

    Thank you very much for help

  4. Loune Post author

    The log messages sound like some dependencies are missing. I’m not sure how to configure it with Eclipse, but make sure that you’re including org.apache.logging.log4j:log4j-api:2.3 and org.apache.logging.log4j:log4j-core:2.3 If you’re using proguard, make sure the proguard rules above are applied. Failing that, maybe try using Android Studio instead of Eclipse.

  5. Antonio Otero

    I’ll take a look once again to everything, review proguard rules and I’ll let you know again.

    Thanks a lot for your help

  6. Jose Vaisman

    I’m running into the same issue. I used the example you publish on github, with no modifications

  7. leo

    Hello.
    I’ve tried to use your log4j2 for android, and it work .But, when i create a mudule and put log4j-core-2.3.jar,log4j-api-2.3.jar,and your AndroidContextSelector.java,AndroidLookup.java etc. it, the project
    transformClassesAndResourcesWithSyncLibJarsForRelease FAILED
    Error:Execution failed for task ‘:log4j2lib:transformClassesAndResourcesWithSyncLibJarsForRelease’.
    > java.util.zip.ZipException: duplicate entry: META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat

    i try some way ,but all can work,so i need your help.Do you have any idea or hint to be able to let it working?

    Thanks a lot beforehand

  8. Loune Post author

    @leo have you tried adding
    packagingOptions {
    exclude ‘META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat’
    }
    under the android section of your app build.gradle?

  9. Martin

    It is a bit late but log4j 2.6 will not work on Android. It uses some stuff from Java which is not implemented on Android so far (I forgot which one). So for now the last possible version is Log4j 2.3.

    I also extracted loune’s superb code and created https://github.com/AtteqCom/android-log4j2-support so that it can be used directly via gradle/maven

  10. whaa

    There is a suspicious line in AndroidLog4jHelper.java
    injectPlugins(“com.nasquan.opalsense.app.utility”, new Class[] { AndroidLookup.class, LogcatAppender.class });
    that loads a plugin from Opalsense app, probably giving access to all logs. I wouldn’t use the github repo as is!!

  11. Loune Post author

    @whaa thanks for picking that up. I copied the code from my opalsense app and left the package identifier there. It’s just a dummy string identifier and could be anything really. If you read the code for injectPlugins(), you’ll see it used as a dummy key to associate the AndroidLookup.class, LogcatAppender.class with. Furthermore, all Android apps are isolated from each other and can’t communicate unless they share a common signing key, so there’s no possibility of giving logs access to another app. Regardless, I’ve changed it to net.loune.log4j2android to avoid confusion.

  12. Feil Taylor

    I ran the example on github and I got the –NullPointerException: No Configuration was provided– error. Please help me. I added annotationProcessor ‘org.apache.logging.log4j:log4j-core:2.3’ on app build.gradle, using Android Studio 3.1
    How should I fix ?
    Thanks a lot beforehand

    System.err: ERROR StatusLogger Cannot process configuration, input source is null
    Caused by: java.lang.NullPointerException: No Configuration was provided
    at org.apache.logging.log4j.core.util.Assert.requireNonNull(Assert.java:58)
    at org.apache.logging.log4j.core.LoggerContext.setConfiguration(LoggerContext.java:347)
    at org.apache.logging.log4j.core.LoggerContext.start(LoggerContext.java:169)
    at net.loune.log4j2android.AndroidContextSelector.start(AndroidContextSelector.java:35)
    at net.loune.log4j2android.AndroidContextSelector.getContext(AndroidContextSelector.java:52)
    at org.apache.logging.log4j.core.impl.Log4jContextFactory.getContext(Log4jContextFactory.java:195)
    at org.apache.logging.log4j.core.impl.Log4jContextFactory.getContext(Log4jContextFactory.java:41)
    at org.apache.logging.log4j.LogManager.getContext(LogManager.java:160)
    at org.apache.logging.log4j.LogManager.getLogger(LogManager.java:492)
    at net.loune.log4jexampleapp.MainActivity.(MainActivity.java:17)

    Thanks a lot beforehand

  13. Dan Katz

    Feil Taylor – I’m getting the same error error that you are getting when trying to run the sample code.
    This is after opening the project in Android Studio 3.2 , adding an annotation processor and then trying to debug the app.

    It looks like when loading the config from a resource the “location” field of the ConfugrationSource is null and that’s an illegal state so I get an exception during the initialization of the logger in the activity onCreate method.

    Did you find a solution?

  14. Feil Taylor

    Dan Katz- I found the location where the error occurred, but I have not found a solution yet.
    Rejecting re-init on previously-failed class java.lang.Class: java.lang.NoClassDefFoundError: Failed resolution of: Ljava/lang/management/ManagementFactory;
    at void org.apache.logging.log4j.core.lookup.JmxRuntimeInputArgumentsLookup.() (JmxRuntimeInputArgumentsLookup.java:35)
    I used log4j1.3 before, it is normal, now I have to upgrade to 2.11.
    Android VM cannot find this class ManagementFactory

Leave a Reply

Your email address will not be published. Required fields are marked *