Android Remote Logging via Timber and Firestore (Kotlin)

July 8, 2019

I wanted to perform remote Android logging for specific users and conditions for release debugging purpose.

Options

Solution

By default, Timber support custom output handlers. Firestore is the easiest method to send data to remote server without the need to write a RESTful API, and there is a simple console UI to view the data.

Remote logging handler for Timber. Only log for

Firestore

  • Use user_log collection with user id as document id. (allow me to debug log messages by user easily)
  • Using single document to store all log messages per user, storing them in map using Timestamp as the key.
  • Using updateOrCreate to make sure partial update is successful even when document does not exist.
  • Since firestore map key must be String, I am using Timestamp.now().toLocalDateTime() and convert it to device local timezone datetime string. I could use UTC as well.
// class RemoteLoggingTree: Timber.DebugTree() {
class RemoteLoggingTree: Timber.Tree() {
    private val watchUids = listOf(
        "QYHqCKCGSbY3qqMSliqNhR3j9QD2", // Desmond
        "CHqMRFyD6OeavAIz1xKx2yLEM3Z2", // Mei Ru
        "iDCeXV2lFCSNBKx94euaTczicyp1"  // JackJack
    )

    private val watchTags = listOf(
        "ReminderWorker"
    )

    override fun isLoggable(tag: String?, priority: Int): Boolean {
        return priority >= Log.INFO
    }

    @SuppressLint("LogNotTimber")
    override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
        if (priority == Log.VERBOSE || priority == Log.DEBUG) {
            return
        }

        val user = FirebaseAuth.getInstance().currentUser
        // tag is null unless .tag() is called
        // override Timber.DebugTree will have performance penalty
        // https://github.com/JakeWharton/timber/issues/122

        if (user != null && watchUids.contains(user.uid) && watchTags.contains(tag)) {
            val db = FirebaseFirestore.getInstance()

            val ref = db.collection("user_log").document(user.uid)

            val priorityString = when(priority) {
                Log.INFO -> "I"
                Log.WARN -> "W"
                Log.ERROR -> "E"
                else -> ""
            }

            // Log.i("RemoteLoggingTree", "log to firestore")
            val logString = "$priorityString/$tag: $message"
            // firestore map key must be String - use device local timestamp
            val key =  Timestamp.now().toLocalDateTime().toString()
            ref.updateOrCreate(mapOf(
                "logs" to mapOf(
                    key to logString
                )
            ))
        }
        else {
            // Log.i("RemoteLoggingTree", "skip log -> $message")
            // super.log(priority, tag, message, t)
        }
    }
}

Enable remote logging for RELEASE only.

class LuaApp: Application() {
    override fun onCreate() {
        if (BuildConfig.DEBUG) {
            Timber.plant(Timber.DebugTree())
        } else {
            Timber.plant(RemoteLoggingTree())
        }
    }
}

Logging

val TAG =  "ReminderWorker"

Timber.tag(TAG).i("doWork: show notification")

// e = Exception
Timber.tag(TAG).e(e)

View logs from Firebase Database Console.

Firebase Database Console

This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.