Timber (Android)
Android logging library by Square. Functions as wrapper for Android standard log API, enabling automatic tag generation, log disabling in production builds, and addition of custom log destinations. Widely adopted in Android development.
Library
Timber
Overview
Timber is "a logger with a small, extensible API" developed by Jake Wharton, providing enhanced functionality over Android's standard Log class for Android applications. Through flexible behavior customization with Tree instances, automatic tag generation (class name, method name, line number), code quality improvement support through lint rules, and full Kotlin compatibility, it achieves an ideal logging experience for Android developers. By combining DebugTree for detailed log output during development and ReleaseTree for controlled logging in production, it supports efficient Android app debugging and production operations.
Details
Timber 2025 edition (v5.x) has been completely rewritten in Kotlin while maintaining full compatibility with 4.x series, aiming for future Kotlin Multiplatform support as the latest Android logging library. The extensible Tree system designed by Jake Wharton's high technical expertise enables detailed stack trace logs in development environments, automatic transmission to Crashlytics in production environments, and custom format output through the same API. Integration with Android Studio Lint automatically detects string format errors, redundant descriptions, inappropriate tag lengths, and more during development, supporting the writing of high-quality logging code.
Key Features
- Tree System: Extensible output destination and format customization functionality
- Automatic Tag Generation: Automatic detection and configuration of class names, method names, and line numbers
- Lint Integration: Development-time logging quality improvement support functionality
- Full Kotlin Support: Latest codebase rewritten in Kotlin
- DebugTree: Detailed log output functionality for development
- Lightweight Design: High-performance logging with minimal overhead
Pros and Cons
Pros
- Upward compatible with Android Log class, minimal learning cost, easy existing code migration
- Flexible output destination customization through Tree system (files, crash reports, etc.)
- No manual tag configuration needed due to automatic tag generation, efficient log identification
- Development-time quality improvement and early bug detection through Lint integration
- High-quality design and active maintenance by Jake Wharton
- High future potential with planned Kotlin Multiplatform support
Cons
- Android platform exclusive, unsuitable for cross-platform development
- Limited advanced structured logging functionality (JSON output, etc.)
- Log level control based on Android Log with constraints for fine-grained control
- No asynchronous logging functionality, I/O performance limitations
- Risk of unintended log output in production if Tree configuration is incorrect
- Third-party library, not Android standard API
Reference Pages
Code Examples
Installation and Setup
// Add dependency to build.gradle (Module: app)
dependencies {
implementation 'com.jakewharton.timber:timber:5.0.1'
}
// Maven repository configuration (usually not required)
repositories {
mavenCentral()
}
// For using snapshot versions
repositories {
mavenCentral()
maven {
url 'https://oss.sonatype.org/content/repositories/snapshots/'
}
}
dependencies {
implementation 'com.jakewharton.timber:timber:5.1.0-SNAPSHOT'
}
// Initialization in Application class
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// Debug build configuration
if (BuildConfig.DEBUG) {
// DebugTree: Detailed information log output
Timber.plant(Timber.DebugTree())
} else {
// Release build configuration (custom Tree)
Timber.plant(ReleaseTree())
}
Timber.d("Timber initialized successfully")
}
}
// Specify Application class in AndroidManifest.xml
/*
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
...
</application>
*/
Basic Log Output
import timber.log.Timber
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Basic log level output
Timber.v("Verbose log - Most detailed information")
Timber.d("Debug log - Debug information")
Timber.i("Info log - General information")
Timber.w("Warning log - Warning information")
Timber.e("Error log - Error information")
// Formatted log output
val userName = "John Doe"
val userId = 12345
Timber.d("User info: %s (ID: %d)", userName, userId)
// Kotlin string templates (recommended)
Timber.i("Current Activity: ${this::class.simpleName}")
Timber.d("Screen size: ${resources.displayMetrics.widthPixels}x${resources.displayMetrics.heightPixels}")
// Exception log output
try {
riskyOperation()
} catch (e: Exception) {
Timber.e(e, "Error occurred during operation")
}
// Tagged log output
Timber.tag("CustomTag").d("Custom tagged log")
Timber.tag("Network").i("API call started")
// Detailed information output with multiple arguments
val startTime = System.currentTimeMillis()
performOperation()
val duration = System.currentTimeMillis() - startTime
Timber.d("Operation completed: duration=%dms, thread=%s", duration, Thread.currentThread().name)
}
private fun riskyOperation() {
// Risky operation simulation
if (Math.random() > 0.5) {
throw RuntimeException("Random error")
}
}
private fun performOperation() {
// Operation simulation
Thread.sleep(100)
}
}
// Fragment log usage example
class UserFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
Timber.d("UserFragment View creation started")
val view = inflater.inflate(R.layout.fragment_user, container, false)
// Get user data
val userId = arguments?.getInt("user_id") ?: -1
Timber.i("Loading data for user ID: %d", userId)
if (userId > 0) {
loadUserData(userId)
} else {
Timber.w("Invalid user ID: %d", userId)
}
Timber.d("UserFragment View creation completed")
return view
}
private fun loadUserData(userId: Int) {
Timber.d("User data loading started: ID=%d", userId)
// Data loading process (simulation)
val userData = mapOf(
"id" to userId,
"name" to "User$userId",
"email" to "user$userId@example.com"
)
Timber.i("User data retrieved successfully: %s", userData)
}
}
Conditional Logging
import timber.log.Timber
class ConditionalLoggingExample {
companion object {
// Helper function for conditional log output
fun logIfDebug(message: String) {
if (BuildConfig.DEBUG) {
Timber.d(message)
}
}
fun logNetworkInfo(enabled: Boolean, message: String) {
if (enabled) {
Timber.tag("Network").d(message)
}
}
}
fun performNetworkRequest(enableVerboseLogging: Boolean) {
Timber.i("Network request started")
// Detailed logs are output conditionally
logNetworkInfo(enableVerboseLogging, "Request URL: https://api.example.com/users")
logNetworkInfo(enableVerboseLogging, "Request Headers: Authorization=Bearer xxx")
try {
// Network processing simulation
val response = simulateNetworkCall()
Timber.i("Network request successful: status=%d", response.statusCode)
logNetworkInfo(enableVerboseLogging, "Response Body: ${response.body}")
} catch (e: Exception) {
Timber.e(e, "Network request failed")
}
}
// Heavy processing executed only in debug builds
fun expensiveDebugOperation(data: Any) {
if (BuildConfig.DEBUG) {
val jsonData = convertToJson(data)
Timber.d("Detailed data:\n%s", jsonData)
}
}
private fun simulateNetworkCall(): NetworkResponse {
Thread.sleep(500) // Network delay simulation
return NetworkResponse(200, """{"users": [{"id": 1, "name": "John Doe"}]}""")
}
private fun convertToJson(data: Any): String {
// JSON conversion process (heavy processing simulation)
return """{"data": "$data", "timestamp": ${System.currentTimeMillis()}}"""
}
data class NetworkResponse(val statusCode: Int, val body: String)
}
// Conditional logging in Fragment lifecycle
class LifecycleLoggingFragment : Fragment() {
private val verboseLifecycleLogging = BuildConfig.DEBUG
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
logLifecycle("onCreate", "Bundle=${savedInstanceState != null}")
}
override fun onStart() {
super.onStart()
logLifecycle("onStart", "Fragment display started")
}
override fun onResume() {
super.onResume()
logLifecycle("onResume", "Fragment is active")
}
override fun onPause() {
super.onPause()
logLifecycle("onPause", "Fragment is inactive")
}
override fun onStop() {
super.onStop()
logLifecycle("onStop", "Fragment is hidden")
}
override fun onDestroy() {
super.onDestroy()
logLifecycle("onDestroy", "Fragment is destroyed")
}
private fun logLifecycle(method: String, details: String) {
if (verboseLifecycleLogging) {
Timber.tag("Lifecycle").d("%s: %s", method, details)
} else {
Timber.d("Lifecycle: %s", method)
}
}
}
Log Level Configuration
import timber.log.Timber
// Custom Tree for log level control
class FilteredDebugTree : Timber.DebugTree() {
companion object {
var minimumLogLevel = Log.VERBOSE
}
override fun isLoggable(tag: String?, priority: Int): Boolean {
return priority >= minimumLogLevel
}
override fun createStackElementTag(element: StackTraceElement): String {
// Custom tag format: ClassName.MethodName:LineNumber
return "${element.className.substringAfterLast('.')}.${element.methodName}:${element.lineNumber}"
}
}
// Log level control usage example
class LogLevelExample {
fun demonstrateLogLevels() {
// Normal log level setting
setLogLevel(Log.INFO)
Timber.v("Verbose log") // Not output
Timber.d("Debug log") // Not output
Timber.i("Info log") // Output
Timber.w("Warning log") // Output
Timber.e("Error log") // Output
// Change log level
setLogLevel(Log.DEBUG)
Timber.d("Debug log 2") // Output
Timber.v("Verbose log 2") // Not output
}
private fun setLogLevel(level: Int) {
FilteredDebugTree.minimumLogLevel = level
val levelName = when (level) {
Log.VERBOSE -> "VERBOSE"
Log.DEBUG -> "DEBUG"
Log.INFO -> "INFO"
Log.WARN -> "WARN"
Log.ERROR -> "ERROR"
else -> "UNKNOWN"
}
Timber.i("Log level set: %s", levelName)
}
}
// Tag-based log level control
class TagBasedLogLevel : Timber.DebugTree() {
private val tagLogLevels = mapOf(
"Network" to Log.INFO,
"Database" to Log.DEBUG,
"UI" to Log.WARN,
"Performance" to Log.VERBOSE
)
override fun isLoggable(tag: String?, priority: Int): Boolean {
val minLevel = tagLogLevels[tag] ?: Log.VERBOSE
return priority >= minLevel
}
}
// Detailed configuration in Application class
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
// Debug build: Detailed logs
Timber.plant(FilteredDebugTree())
FilteredDebugTree.minimumLogLevel = Log.DEBUG
} else {
// Release build: Error logs only
Timber.plant(ReleaseTree())
}
}
}
// Release Tree (error logs only)
class ReleaseTree : Timber.Tree() {
override fun isLoggable(tag: String?, priority: Int): Boolean {
// Output only errors and warnings
return priority >= Log.WARN
}
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
if (isLoggable(tag, priority)) {
// Send to Crashlytics or log collection system
when (priority) {
Log.ERROR -> {
// Send error logs to crash reports
logErrorToCrashlytics(tag, message, t)
}
Log.WARN -> {
// Send warning logs to analytics system
logWarningToAnalytics(tag, message)
}
}
}
}
private fun logErrorToCrashlytics(tag: String?, message: String, throwable: Throwable?) {
// Send error logs using Crashlytics SDK
// FirebaseCrashlytics.getInstance().recordException(throwable ?: Exception(message))
println("Crashlytics: [$tag] $message")
}
private fun logWarningToAnalytics(tag: String?, message: String) {
// Send warning logs using Analytics SDK
// Send events to Firebase Analytics or Google Analytics
println("Analytics: [$tag] $message")
}
}
Android-Specific Features
import timber.log.Timber
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import java.io.File
import java.io.FileWriter
import java.text.SimpleDateFormat
import java.util.*
// Android-specific custom Tree implementation
class AndroidFileTree(private val context: Context) : Timber.Tree() {
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
val priorityString = when (priority) {
Log.VERBOSE -> "V"
Log.DEBUG -> "D"
Log.INFO -> "I"
Log.WARN -> "W"
Log.ERROR -> "E"
else -> "?"
}
val timestamp = dateFormat.format(Date())
val logLine = "$timestamp $priorityString/$tag: $message\n"
// Save log file to internal storage
saveLogToFile(logLine)
// Additional save if exception information exists
t?.let {
val stackTrace = Log.getStackTraceString(it)
saveLogToFile("$timestamp $priorityString/$tag: Exception:\n$stackTrace\n")
}
}
private fun saveLogToFile(logLine: String) {
try {
val logDir = File(context.filesDir, "logs")
if (!logDir.exists()) {
logDir.mkdirs()
}
val logFile = File(logDir, "app_log_${getCurrentDate()}.txt")
FileWriter(logFile, true).use { writer ->
writer.write(logLine)
}
} catch (e: Exception) {
// Output to standard log in case of file save error
Log.e("AndroidFileTree", "Log file save error", e)
}
}
private fun getCurrentDate(): String {
return SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
}
}
// Log configuration management using SharedPreferences
class LoggingPreferencesManager(private val context: Context) {
private val preferences: SharedPreferences =
context.getSharedPreferences("logging_prefs", Context.MODE_PRIVATE)
var enableFileLogging: Boolean
get() = preferences.getBoolean("enable_file_logging", false)
set(value) = preferences.edit().putBoolean("enable_file_logging", value).apply()
var logLevel: Int
get() = preferences.getInt("log_level", Log.DEBUG)
set(value) = preferences.edit().putInt("log_level", value).apply()
var enableNetworkLogging: Boolean
get() = preferences.getBoolean("enable_network_logging", true)
set(value) = preferences.edit().putBoolean("enable_network_logging", value).apply()
fun configureTimber() {
// Clear existing Trees
Timber.uprootAll()
if (BuildConfig.DEBUG) {
// Debug build configuration
Timber.plant(FilteredDebugTree().apply {
FilteredDebugTree.minimumLogLevel = logLevel
})
if (enableFileLogging) {
Timber.plant(AndroidFileTree(context))
}
} else {
// Release build configuration
Timber.plant(ReleaseTree())
}
Timber.i("Timber configuration completed: FileLogging=%s, LogLevel=%d, NetworkLogging=%s",
enableFileLogging, logLevel, enableNetworkLogging)
}
}
// Android-specific feature usage example in Activity
class MainActivity : AppCompatActivity() {
private lateinit var loggingPrefs: LoggingPreferencesManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
loggingPrefs = LoggingPreferencesManager(this)
loggingPrefs.configureTimber()
// Activity lifecycle logs
Timber.tag("ActivityLifecycle").i("MainActivity.onCreate")
// System information logs
logSystemInfo()
// Network state logs
logNetworkState()
// App information logs
logAppInfo()
}
private fun logSystemInfo() {
Timber.tag("SystemInfo").apply {
d("Android version: ${android.os.Build.VERSION.RELEASE}")
d("API level: ${android.os.Build.VERSION.SDK_INT}")
d("Device: ${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}")
d("Screen density: ${resources.displayMetrics.density}")
d("Available memory: ${getAvailableMemory()}MB")
}
}
private fun logNetworkState() {
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val networkInfo = connectivityManager.activeNetworkInfo
Timber.tag("Network").apply {
if (networkInfo?.isConnected == true) {
i("Network connected: %s", networkInfo.typeName)
} else {
w("Network disconnected")
}
}
}
private fun logAppInfo() {
try {
val packageInfo = packageManager.getPackageInfo(packageName, 0)
Timber.tag("AppInfo").apply {
i("App version: %s (%d)", packageInfo.versionName, packageInfo.versionCode)
i("Package name: %s", packageName)
i("Debug build: %s", BuildConfig.DEBUG)
}
} catch (e: Exception) {
Timber.e(e, "App information retrieval error")
}
}
private fun getAvailableMemory(): Long {
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val memoryInfo = ActivityManager.MemoryInfo()
activityManager.getMemoryInfo(memoryInfo)
return memoryInfo.availMem / (1024 * 1024) // Convert to MB
}
}
Performance Considerations
import timber.log.Timber
// Log control for performance optimization
class PerformanceOptimizedLogging {
companion object {
// Log throttling for frequent calls
private var lastLogTime = 0L
private const val LOG_INTERVAL_MS = 1000L // 1-second interval
fun logWithThrottling(tag: String, message: String) {
val currentTime = System.currentTimeMillis()
if (currentTime - lastLogTime >= LOG_INTERVAL_MS) {
Timber.tag(tag).d(message)
lastLogTime = currentTime
}
}
// Lazy log evaluation (for heavy processing)
inline fun logIfEnabled(priority: Int, tag: String, lazyMessage: () -> String) {
if (Timber.isLoggable(tag, priority)) {
when (priority) {
Log.DEBUG -> Timber.tag(tag).d(lazyMessage())
Log.INFO -> Timber.tag(tag).i(lazyMessage())
Log.WARN -> Timber.tag(tag).w(lazyMessage())
Log.ERROR -> Timber.tag(tag).e(lazyMessage())
}
}
}
}
fun demonstratePerformanceOptimizations() {
// ✓ Good example: Simple string log
Timber.d("Process started")
// ✓ Good example: Lightweight string template
val userId = 123
Timber.d("User ID: %d", userId)
// ✗ Example to avoid: Directly including heavy processing
// Timber.d("Heavy process result: ${expensiveOperation()}")
// ✓ Good example: Conditional heavy processing execution
if (BuildConfig.DEBUG) {
val result = expensiveOperation()
Timber.d("Heavy process result: %s", result)
}
// ✓ Good example: Lazy evaluation
logIfEnabled(Log.DEBUG, "Performance") {
"Complex calculation result: ${complexCalculation()}"
}
// Large data performance test
performanceBenchmark()
}
private fun expensiveOperation(): String {
// Heavy processing simulation
Thread.sleep(100)
return "Process completed"
}
private fun complexCalculation(): Double {
// Complex calculation simulation
return (1..1000).map { Math.random() }.average()
}
private fun performanceBenchmark() {
val startTime = System.currentTimeMillis()
// Large log output test
repeat(1000) { index ->
// Log throttling
if (index % 100 == 0) {
Timber.d("Progress: %d/1000", index)
}
// Lightweight processing
processItem(index)
}
val duration = System.currentTimeMillis() - startTime
Timber.i("Performance test completed: %dms", duration)
}
private fun processItem(index: Int) {
// Item processing simulation
if (index < 10) {
Timber.v("Item %d processing", index)
}
}
}
// Memory-efficient log management
class MemoryEfficientLogging {
private val logBuffer = mutableListOf<String>()
private val maxBufferSize = 100
fun addLog(message: String) {
logBuffer.add(message)
// Buffer size limitation
if (logBuffer.size > maxBufferSize) {
logBuffer.removeAt(0) // Remove old logs
}
}
fun flushLogs() {
if (logBuffer.isNotEmpty()) {
Timber.d("Buffer log output: %d items", logBuffer.size)
logBuffer.forEach { Timber.d(it) }
logBuffer.clear()
}
}
// Log context retention using WeakReference
private val contextReferences = mutableMapOf<String, WeakReference<Any>>()
fun logWithContext(key: String, context: Any, message: String) {
contextReferences[key] = WeakReference(context)
Timber.tag(key).d("%s [Context: %s]", message, context::class.simpleName)
// Cleanup invalid references
cleanupReferences()
}
private fun cleanupReferences() {
val iterator = contextReferences.iterator()
while (iterator.hasNext()) {
val entry = iterator.next()
if (entry.value.get() == null) {
iterator.remove()
}
}
}
}
// Performance measurement with custom Tree
class PerformanceTree : Timber.DebugTree() {
private val logCounts = mutableMapOf<String, Int>()
private val logTimes = mutableMapOf<String, Long>()
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
val startTime = System.nanoTime()
super.log(priority, tag, message, t)
val endTime = System.nanoTime()
val duration = endTime - startTime
// Performance statistics collection
tag?.let {
logCounts[it] = logCounts.getOrDefault(it, 0) + 1
logTimes[it] = logTimes.getOrDefault(it, 0L) + duration
}
}
fun printStatistics() {
Timber.d("=== Log Performance Statistics ===")
logCounts.forEach { (tag, count) ->
val totalTime = logTimes[tag] ?: 0L
val averageTime = if (count > 0) totalTime / count else 0L
Timber.d("Tag: %s, Count: %d, Average: %d ns", tag, count, averageTime)
}
}
}