Tom Zurkan
5 min readMay 13, 2022

--

Android and Logback play well together

Using Logback with Android to Extend or Enhance your Logging

If you are using any jars or sdks in your app, then you are more than likely familiar with slf4j. Slf4J is an abstraction layer for your logging implementation. So, most SDKs would choose slf4j-api and allow the application to do the implementation using something like log4j or logback.

Today, we are going to talk about logback briefly as a way to proxy your logging by using appenders. Why would you want to do that? You can use the appender to send specific errors to your splunk log or some other service. Or, you could filter out specific log messages.

Implementing appenders is pretty straightforward. The logback implementation uses appenders to do the actual logging. And, you can add appenders. You provide a configuration file, usually logback.xml and that is picked up with your logback initialization and used.

There are two ways you can implement your logback.xml. Sometimes, there is already a logback.xml file already in your classpath. In that case, you would not name your logback file logback.xml but something unique (i.e. mylogback.xml). Below we will discuss implementing first straight logback.xml and then how to load your own logback config programmatically.

In Android, all you need to do is create a new Android Resource Directory “src/main/resource” and add logback.xml under that resource. Logback will automatically pick up that “file” (I say file in quotes because apparently apk does not actually have files per say). Below are some illustrations to show the setup and usage of logback.

Below is an example logback.xml file:

<configuration>
<appender name="MYAPPENDER" class="com.mypackage.demoapp.MyAppender"/>
<root level="info">
<appender-ref ref="MYAPPENDER"/>
</root>
<logger name="com.package.i.want.to.override" additivity="false">
<appender-ref ref="MYAPPENDER" />
</logger>
</configuration>

MyAppender is in my package. The logger “override” is for whatever package I want to take log messages from. The additivity keyword is whether it should be added to the appenders list or be the only appender (for instance, should it go on to console). In the case above, I am saying I want messages from com.package.i.want.to.override and then I use my appender. Below is the appender code:

package com.mypackage.demoapp

import android.content.Context
import android.util.Log
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.core.AppenderBase
import ch.qos.logback.classic.Logger;
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
import org.slf4j.LoggerFactory;
import java.io.File
import java.io.IOException
class MyAppender : AppenderBase<ILoggingEvent>() {
var appContext: Context? = null
override fun append(eventObject: ILoggingEvent?) {
eventObject?.let {
Log.i("i","\uD83D\uDC36\uD83D\uDC2E " + it.formattedMessage)
}
}
}

Never-mind all those imports. We will talk more about them shortly. For now, you can see that I have created an appender for all info log messages. The logger just logs it to the console via android logging with a dog and cow. Something like this:

I/i: 🐶🐮 event=CLIENT_INITIALIZATION, message=SOME_PROPERTIES, WERE SET

That’s it and it is pretty amazing how easy and slick it is to set up. But, sometimes there is another logback.xml file in your classpath that you cannot access and is picked up during build time. What do you do then? Better yet, what if your service relies on the application context so your appender needs the context in order to complete the splunk call possibly through an intent?

First, instead of putting the file in the resources directory, you can move it to the assets directory and call it mylogback.xml. If you don’t have an Android Assets Directory, create one. As I mentioned above, you cannot access a file directly within your apk. So, you need to read an asset and then write it out to a temporary file. That is why this part is so tricky in Android.

Your logback file should look something like this:

Now, your appender needs to be loaded. Below is my example of the appender with functions to handle loading as well:

package com.mypackage.demoapp

import android.content.Context
import android.util.Log
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.core.AppenderBase
import ch.qos.logback.classic.Logger;
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
import org.slf4j.LoggerFactory;
import java.io.File
import java.io.IOException

class MyAppender : AppenderBase<ILoggingEvent>() {
var appContext: Context? = null
override fun append(eventObject: ILoggingEvent?) {
eventObject?.let {
SplunkLogger.instance.log(appContext, "MyLog", it.formattedMessage)
}
}

companion object Register {
fun registerAppender(context: Context) {

// assume SLF4J is bound to logback in the current environment
val logContext = LoggerFactory.getILoggerFactory() as LoggerContext

try {
val configurator = JoranConfigurator()
configurator.setContext(logContext)
// Call context.reset() to clear any previous configuration, e.g. default
// configuration. For multi-step configuration, omit calling context.reset().
logContext.reset()

val file = getFileFromAssets(context, "mylogback.xml")
configurator.doConfigure(file)

val log = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as Logger
val app = log.getAppender("MYAPPENDER") as MyAppender
app.appContext = context
} catch (je: JoranException) {
// StatusPrinter will handle this
print(je)
StatusPrinter.printInCaseOfErrorsOrWarnings(logContext)
}

}

@Throws(IOException::class)
fun getFileFromAssets(context: Context, fileName: String): File = File(context.cacheDir, fileName)
.also {
if (!it.exists()) {
it.outputStream().use { cache ->
context.assets.open(fileName).use { inputStream ->
inputStream.copyTo(cache)
}
}
}
}
}
}

Let’s walk through what our appender contains now. We have added a static method registerAppender that takes the application context.

The registerAppender sets the logging context (not application context) for a configurator. Here I clear out the previous context by calling logContext.reset(). If I didn’t call logContext.reset(), my configuration would just be added to the current configuration which might be what you want.

For a file used for configuration, I simply look if I have a cache file by that name and if not create one with the asset with that name. So, that gives us a file to hand to the configurator to configure.

Then after configuration, I can get the appender I’ve added and set the application context if I need to (if not, I just registered the appender). The caveat for this approach is that the register should be called before the package the appender is intended for is used.

I hope that this helps you with using slf4j and logback. I wrote this article because I couldn’t find anything on using logback native on Android. Appenders are a lightweight solution to making sure the log messages that are important to you are findable. It is also a great way to reduce noise of other packages that may be producing too much logging.

All the best and Happy Coding!

--

--

Tom Zurkan

Engineer working on full stack. His interests lie in modern languages and how best to develop elegant crash proof code.