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.
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")
}
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"
}
}
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.
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")
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
}
| BunnyLogs field | Source |
|---|---|
message | [tag] message (tag omitted when null) |
level | Mapped from Android log priority (VERBOSE…ASSERT) |
program | program constructor parameter (default: android) |
timestamp | Server receipt time (no client timestamp sent) |
enqueue dispatches on its own thread pool — log() returns immediately and never blocks the calling thread.OkHttpClient instance across your whole app; do not create a new one per tree or per call.BuildConfig or a secrets file rather than hardcoding it in source.level=ERROR and program=android to get notified on errors via Slack, Telegram, or Discord.