Enhancing Android Logging: Add JSON Formatting to Timber with Moshi

Android|Apr 16, 2023|Last edited: May 4, 2023
  • Moshi
  • Gson
  • JSON
  • Timber
  • Logging
  • Android Studio
  • Enhancing Android Logging: Add JSON Formatting to Timber with Moshi
    Apr 16, 2023
    Improve Timber log readability in Android by adding JSON formatting with Moshi library. Our guide shows how to create a custom DebugTree class for efficient debugging.
    Android Studio

    Prettify JSON Logs with Timber and Moshi in Your Android Applications

    In Android development, logging is crucial for debugging and understanding how your application behaves during its lifecycle. Timber is a popular and powerful logging library for Android that simplifies the logging process and provides additional features like customizing log output. In this blog post, we will show you how to extend the Timber library's capabilities by adding JSON formatting (prettification) to your logs.


    Before diving into the implementation, make sure you have a basic understanding of Timber and how to set it up in your Android project. You can refer to this article for more information on this topic: https://www.androiddevnotes.com/article/android-logging-clickable-line-numbers-timber.

    Implementation Overview

    To add JSON formatting to your Timber logs, we will create a custom DebugTree, add the JSON formatting logic, and plant it in our Application class. We will use either Moshi or Gson, both popular options for JSON parsing and serialization in Android. In this post, we will primarily focus on Moshi implementation, but we will also provide code for Gson if it helps anyone.

    Step 1: Create a custom DebugTree class

    First, we need to create a custom DebugTree class, which we'll call BetterTimberDebugTree. This class will inherit from Timber.DebugTree, and we'll override the log method to include the JSON formatting logic.
    import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import timber.log.Timber import java.util.regex.Pattern class BetterTimberDebugTree(private val globalTag: String = "GTAG") : Timber.DebugTree() { private val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() private val jsonPattern: Pattern = Pattern.compile("(\\{(?:[^{}]|(?:\\{(?:[^{}]|(?:\\{[^{}]*\\}))*\\}))*\\})") override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { findLogCallStackTraceElement()?.let { element -> val lineNumberInfo = "(${element.fileName}:${element.lineNumber})" val formattedMessage = formatJsonIfNeeded(message) val updatedMessage = "$lineNumberInfo: $formattedMessage" super.log(priority, "$globalTag-$tag", updatedMessage, t) } ?: run { super.log(priority, "$globalTag-$tag", message, t) } } override fun createStackElementTag(element: StackTraceElement): String? { return element.fileName } private fun findLogCallStackTraceElement(): StackTraceElement? { val stackTrace = Throwable().stackTrace var foundDebugTree = false return stackTrace.firstOrNull { element -> if (element.className.contains("BetterTimberDebugTree")) { foundDebugTree = true false } else { foundDebugTree && !element.className.contains("Timber") } } } private fun formatJsonIfNeeded(message: String): String { val matcher = jsonPattern.matcher(message) val buffer = StringBuffer() while (matcher.find()) { try { val jsonAdapter: JsonAdapter<Any> = moshi.adapter(Any::class.java).indent(" ") val parsedObject = jsonAdapter.fromJson(matcher.group()) val formattedJson = jsonAdapter.toJson(parsedObject) matcher.appendReplacement(buffer, formattedJson) } catch (e: Exception) { // Ignore and continue with the next JSON object } } matcher.appendTail(buffer) return buffer.toString() } }

    Step 2: How JSON formatting logic works

    Next, we need to define the logic that formats the JSON objects within the log messages.
    For this, we use the Moshi library and create a Moshi instance with the KotlinJsonAdapterFactory added:
    private val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
    We also define a regular expression pattern to match JSON objects within the text:
    private val jsonPattern: Pattern = Pattern.compile("(\\{(?:[^{}]|(?:\\{(?:[^{}]|(?:\\{[^{}]\\}))\\}))*\\})")
    Then, we define a formatJsonIfNeeded(message: String) function that processes the input text, finds JSON objects within it, formats them using Moshi, and returns the text with the formatted JSON objects.

    Step 3: Plant the custom DebugTree in your Application class

    After creating the custom DebugTree and adding the JSON formatting logic, you need to plant this tree in your Application class. This will ensure that all Timber logs use your custom DebugTree for logging.
    package com.example.bettertimberlogsandroid import BetterTimberDebugTree import android.app.Application import timber.log.Timber class App : Application() { override fun onCreate() { super.onCreate() Timber.plant(BetterTimberDebugTree("GTAG")) Timber.d("Hello Timber") } }


    Now, you can create Timber logs with random text and JSON objects. The JSON objects within the logs will be prettified automatically:
    class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Timber.d("Hello MainActivity") val sampleJson = """some random non-json string ;adn{[][ {"name":"John Doe","age":30,"isStudent":false,"courses":["mathematics","history","chemistry"],"address":{"street":"123 Main St","city":"New York","state":"NY","postalCode":"10001"}} """.trimIndent() Timber.d(sampleJson) ... } }
    With this setup, even if you have non-JSON strings within the log message, the proper JSON objects will still be prettified just fine.

    Demo of Timber Log message with JSON formatted in Logcat

    notion image

    Demo of Pretty JSON of your Timber Logs in Logcat


    Prettifying Network API Responses in Logs

    Let’s say the response you get when you log your response from a Movie API is similar to:
    [Movie(id=1, title=Inception, year=2010, rating=8.8, director=Christopher Nolan)]
    This is not a JSON, but it would be useful if we could display the response content in a more readable manner in Logcat.
    To do that, use this extension function:
    import com.squareup.moshi.Moshi inline fun <reified T : Any> T.toJson(): String { val moshi = Moshi.Builder().build() val adapter = moshi.adapter(T::class.java) return adapter.toJson(this) }
    Using toJson in your code example:
    Now, when you use Timber to log your response, you will get a prettified display in your Logcat. For example:
    [{ "id": 1, "title": "Inception", "year": 2010, "rating": 8.8, "director": "Christopher Nolan" }]


    In this blog post, we demonstrated how to enhance your Timber logs by adding JSON formatting capabilities. By creating a custom DebugTree class and incorporating Moshi for JSON parsing and formatting, you can improve the readability of your logs and make debugging your Android applications more efficient.


    Clickable Line Numbers in Timber Logs: Boosting Your Android Debugging ExperienceMastering Location-Based App Testing with Maestro UI Testing Framework's Travel Command