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.

LoggingAndroidMobileSimpleCustomizable

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)
        }
    }
}