Android (Timber)

Route Android log output to BunnyLogs in real time using a custom Timber Tree — fire-and-forget HTTP POSTs on a background thread, no blocking.

Dependencies

Add Timber and OkHttp to your build.gradle (or build.gradle.kts). Both are standard in most Android projects:

dependencies {
    implementation("com.jakewharton.timber:timber:5.0.1")
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
}
BunnyLogsTree

Create a Timber.Tree that POSTs each log record to your stream:

import android.util.Log
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import timber.log.Timber
import java.io.IOException

class BunnyLogsTree(
    private val client: OkHttpClient,
    private val uuid: String,
    private val program: String = "android",
) : Timber.Tree() {

    override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
        val body = JSONObject().apply {
            put("message", if (tag != null) "[$tag] $message" else message)
            put("level", priorityName(priority))
            put("program", program)
        }.toString()

        val request = Request.Builder()
            .url("https://bunnylogs.com/live/$uuid")
            .post(body.toRequestBody("application/json".toMediaType()))
            .build()

        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) { /* swallow */ }
            override fun onResponse(call: Call, response: Response) { response.close() }
        })
    }

    private fun priorityName(priority: Int) = when (priority) {
        Log.VERBOSE -> "VERBOSE"
        Log.DEBUG   -> "DEBUG"
        Log.INFO    -> "INFO"
        Log.WARN    -> "WARN"
        Log.ERROR   -> "ERROR"
        Log.ASSERT  -> "ASSERT"
        else        -> "UNKNOWN"
    }
}
Setup

Plant the tree in your Application.onCreate():

import okhttp3.OkHttpClient
import timber.log.Timber

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()

        Timber.plant(Timber.DebugTree())   // keep local logcat

        if (!BuildConfig.DEBUG) {
            val client = OkHttpClient()
            Timber.plant(BunnyLogsTree(client, uuid = "<your-uuid>"))
        }
    }
}

Replace <your-uuid> with the UUID from your stream URL (https://bunnylogs.com/live/<uuid>). The example above only plants the remote tree in release builds — adjust to suit your workflow.

Usage

Log anywhere in your app with standard Timber calls — no import changes needed:

Timber.i("User signed in: %s", userId)
Timber.w("Cache miss — fetching from network")
Timber.e(exception, "Payment failed")
Filtering by level

Override isLoggable() in the tree to only send warnings and errors to BunnyLogs in production:

override fun isLoggable(tag: String?, priority: Int): Boolean {
    return priority >= Log.WARN
}
Field mapping
BunnyLogs fieldSource
message[tag] message (tag omitted when null)
levelMapped from Android log priority (VERBOSEASSERT)
programprogram constructor parameter (default: android)
timestampServer receipt time (no client timestamp sent)
Notes
  • OkHttp's enqueue dispatches on its own thread pool — log() returns immediately and never blocks the calling thread.
  • Share one OkHttpClient instance across your whole app; do not create a new one per tree or per call.
  • Store the UUID in BuildConfig or a secrets file rather than hardcoding it in source.
  • High-frequency apps (e.g. per-frame logs) should batch records before POSTing to avoid excessive requests.
  • Set up an Alert in BunnyLogs matching level=ERROR and program=android to get notified on errors via Slack, Telegram, or Discord.