Safe production logging

To log in production code is to expose your apps’ inner secrets. One tends to log information that is of maximum value for the developer(s), but that can also be of great value for a person with malicious intent. Let’s not do that.

The developer guide on preparing an .apk for release is clear on the point that no Log() statements should be left in any source file that is to be released for production. But we don’t want to manually delete all Log() statements prior to each release. Let’s take a look at how we can keep all logging code without exposing ourselves to the outside world.

In essence, this is really easy. Check what build config we are in and only allow logging when in DEBUG mode. This might look something like

private inline fun inDebug(action: () -> Unit) {
    if (BuildConfig.DEBUG) {
        action()
    }
}

With this we can write a wrapper around the built in Log.java and apply our inDebug to each Log() method. But let’s go one step further and make the log tag optional! We end up with Logger.kt according to

object Logger {
    fun v(tag: String = callerTag(), message: () -> String) = inDebug {
        Log.v(tag, message())
    }

    fun v(tag: String = callerTag(), message: () -> String, exception: Exception) = inDebug {
        Log.v(tag, message(), exception)
    }

    fun d(tag: String = callerTag(), message: () -> String) = inDebug {
        Log.d(tag, message())
    }

    fun d(tag: String = callerTag(), message: () -> String, exception: Exception) = inDebug {
        Log.d(tag, message(), exception)
    }

    fun i(tag: String = callerTag(), message: () -> String) = inDebug {
        Log.i(tag, message())
    }

    fun i(tag: String = callerTag(), message: () -> String, exception: Exception) = inDebug {
        Log.i(tag, message(), exception)
    }

    fun w(tag: String = callerTag(), message: () -> String) = inDebug {
        Log.w(tag, message())
    }

    fun w(tag: String = callerTag(), message: () -> String, exception: Exception) = inDebug {
        Log.w(tag, message(), exception)
    }

    fun e(tag: String = callerTag(), message: () -> String) = inDebug {
        Log.e(tag, message())
    }

    fun e(tag: String = callerTag(), message: () -> String, exception: Exception) = inDebug {
        Log.e(tag, message(), exception)
    }

    fun wtf(tag: String = callerTag(), message: () -> String) = inDebug {
        Log.wtf(tag, message())
    }

    fun wtf(tag: String = callerTag(), message: () -> String, exception: Exception) = inDebug {
        Log.wtf(tag, message(), exception)
    }

    private inline fun inDebug(action: () -> Unit) {
        if (BuildConfig.DEBUG) {
            action()
        }
    }

    /**
     * @return The class name for the calling class as a String.
     */
    private fun callerTag(): String {
        val callStackIndex = 2
        val maxTagLength = 23
        val anonymousClassPattern = Pattern.compile("(\\$\\d+)+$")

        val stackTrace = Throwable().stackTrace
        val callerElement = stackTrace[callStackIndex]
        var tag = callerElement.className
        val matcher = anonymousClassPattern.matcher(tag)

        if (matcher.find()) {
            tag = matcher.replaceAll("")
        }

        tag = tag.substring(tag.lastIndexOf('.') + 1)

        // Tag length limit was removed in API 24.
        return if (tag.length <= maxTagLength || Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            tag
        } else tag.substring(0, maxTagLength)
    }
}

You use the method as Logger.d { "Bar message" }, and the tag will automatically be set to your calling class. Or, supply your own tag with Logger.d("FooTag", { "Bar message" }).

Why the lambda expression?

Now, you might wonder “why is the message string in a lambda expression?” I’m glad you asked!

It all comes down to garbage collection. Making the message argument () -> String makes the argument evaluation lazy. Imagine a statement in the likes of

Log.d("FooTag", "bar string" + crazyHeavyStuff() + "more strings: " + barInt.toString())

This would allocate a StringBuilder for a char[], which would probably be resized a couple of times. Followed by a few StringBuilder.append calls. This would all create a bunch of garbage. Our lazy lambda keeps the garbage to one lazy object only!

Calling Logger from Java

There’s no problem in calling this Kotlin code from Java, but if you need to do so you probably want to add the @JvmStatic annotation like

@JvmStatic fun debug(tag: String = callerTag(), message: () -> String) = inDebug {
    Log.debug(tag, message())
}

You don’t have to, but otherwise you’ll need to call the methods with Logger.INSTANCE.debug(), which is ugly.

Closing

I find myself adding this file to all projects I write and I hope you’ll find it useful as well. There’s also a Gist if you want the full file with the import statements included.