Enhancing Android Logging: Add JSON Formatting to Timber with Moshi
type
Post
status
Published
date
Apr 16, 2023
slug
android-logging-json-formatting-timber-moshi
summary
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.
tags
Moshi
Gson
JSON
Timber
Logging
Android Studio
category
Android
icon
password
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.
Prerequisites
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") } }
Usage
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

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:Timber.d(response.body()?.movie.toJson())
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" }]
Conclusion
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.Reference
Code for Gson implementation: https://github.com/androiddevnotes/better-timber-logs-android/blob/52b51dd952b3a4ec43f2da0d6a4e90bbaf0a78f6/app/src/main/java/com/example/bettertimberlogsandroid/ClickableLineNumberDebugTreeWithGson.kt
Android App Code Example: https://github.com/androiddevnotes/better-timber-logs-android/tree/52b51dd952b3a4ec43f2da0d6a4e90bbaf0a78f6