Timber (Android)
Square社によるAndroid向けロギングライブラリ。Android標準ログAPIのラッパーとして機能し、タグの自動生成、本番ビルドでのログ無効化、カスタムログ出力先の追加が可能。Android開発で広く採用されている。
ライブラリ
Timber
概要
TimberはJake Wharton氏によって開発された「Android向けの小さく拡張可能なAPIを持つロガー」として、AndroidアプリケーションのAndroid標準Log クラスの上位互換機能を提供します。Tree インスタンスによる柔軟な動作カスタマイズ、自動タグ生成(クラス名・メソッド名・行番号)、lint ルールによるコード品質向上支援、Kotlin完全対応など、Android開発者にとって理想的なロギング体験を実現。DebugTreeによる開発時の詳細ログ出力とReleaseTree による本番時の制御されたロギングを組み合わせることで、効率的なAndroidアプリデバッグとプロダクション運用を支援します。
詳細
Timber 2025年版(v5.x)はKotlinで完全に書き直されながらも4.x系との完全な互換性を維持し、将来的なKotlin Multiplatform対応を目指している最新Android ロギングライブラリです。Jake Wharton氏の高い技術力により設計された拡張可能なTree システムにより、開発環境では詳細なスタックトレース付きログ、本番環境ではCrashlytics等への自動送信、カスタム フォーマット出力等を同一APIで実現可能。AndroidStudio のLint 統合により、文字列フォーマットエラー、冗長な記述、不適切なタグ長等を開発時に自動検出し、高品質なロギングコードの記述を支援します。
主な特徴
- Tree システム: 拡張可能な出力先・フォーマット カスタマイズ機能
- 自動タグ生成: クラス名・メソッド名・行番号の自動検出と設定
- Lint 統合: 開発時のロギング品質向上支援機能
- Kotlin 完全対応: Kotlin で書き直された最新コードベース
- DebugTree: 開発用の詳細ログ出力機能
- 軽量設計: 最小限のオーバーヘッドによる高性能ロギング
メリット・デメリット
メリット
- Android Log クラスの上位互換で学習コスト最小、既存コード移行容易
- Tree システムによる柔軟な出力先カスタマイズ(ファイル、クラッシュレポート等)
- 自動タグ生成により手動タグ設定不要、ログ特定の効率化
- Lint 統合による開発時品質向上とバグ早期発見
- Jake Wharton 氏による高品質な設計とアクティブなメンテナンス
- Kotlin Multiplatform 対応予定で将来性が高い
デメリット
- Android プラットフォーム専用でクロスプラットフォーム開発には不向き
- 高度な構造化ロギング機能は限定的(JSON出力等)
- ログ レベル制御が Android Log ベースで細かな制御に制約
- 非同期ロギング機能なくI/O パフォーマンス制限
- Tree 設定を誤ると本番環境で意図しないログ出力リスク
- サードパーティライブラリのため Android 標準APIでない
参考ページ
書き方の例
インストール・セットアップ
// build.gradle (Module: app) への依存関係追加
dependencies {
implementation 'com.jakewharton.timber:timber:5.0.1'
}
// Mavenリポジトリ設定(通常は不要)
repositories {
mavenCentral()
}
// スナップショット版を使用する場合
repositories {
mavenCentral()
maven {
url 'https://oss.sonatype.org/content/repositories/snapshots/'
}
}
dependencies {
implementation 'com.jakewharton.timber:timber:5.1.0-SNAPSHOT'
}
// Application クラスでの初期化
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// Debug ビルドでの設定
if (BuildConfig.DEBUG) {
// DebugTree: 詳細な情報付きログ出力
Timber.plant(Timber.DebugTree())
} else {
// Release ビルドでの設定(カスタムTree)
Timber.plant(ReleaseTree())
}
Timber.d("Timber initialized successfully")
}
}
// AndroidManifest.xml でApplication クラス指定
/*
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
...
</application>
*/
基本的なログ出力
import timber.log.Timber
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 基本的なログレベル別出力
Timber.v("Verbose ログ - 最も詳細な情報")
Timber.d("Debug ログ - デバッグ情報")
Timber.i("Info ログ - 一般的な情報")
Timber.w("Warning ログ - 警告情報")
Timber.e("Error ログ - エラー情報")
// フォーマット付きログ出力
val userName = "田中太郎"
val userId = 12345
Timber.d("ユーザー情報: %s (ID: %d)", userName, userId)
// Kotlin 文字列テンプレート(推奨)
Timber.i("現在の Activity: ${this::class.simpleName}")
Timber.d("画面サイズ: ${resources.displayMetrics.widthPixels}x${resources.displayMetrics.heightPixels}")
// 例外付きログ出力
try {
riskyOperation()
} catch (e: Exception) {
Timber.e(e, "操作中にエラーが発生しました")
}
// タグ付きログ出力
Timber.tag("CustomTag").d("カスタムタグ付きログ")
Timber.tag("Network").i("API 呼び出し開始")
// 複数引数での詳細情報出力
val startTime = System.currentTimeMillis()
performOperation()
val duration = System.currentTimeMillis() - startTime
Timber.d("操作完了: 実行時間=%dms, スレッド=%s", duration, Thread.currentThread().name)
}
private fun riskyOperation() {
// リスクのある操作のシミュレーション
if (Math.random() > 0.5) {
throw RuntimeException("ランダムエラー")
}
}
private fun performOperation() {
// 操作のシミュレーション
Thread.sleep(100)
}
}
// Fragment でのログ使用例
class UserFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
Timber.d("UserFragment の View 作成開始")
val view = inflater.inflate(R.layout.fragment_user, container, false)
// ユーザーデータ取得
val userId = arguments?.getInt("user_id") ?: -1
Timber.i("ユーザーID: %d のデータを取得中", userId)
if (userId > 0) {
loadUserData(userId)
} else {
Timber.w("無効なユーザーID: %d", userId)
}
Timber.d("UserFragment の View 作成完了")
return view
}
private fun loadUserData(userId: Int) {
Timber.d("ユーザーデータ読み込み開始: ID=%d", userId)
// データ読み込み処理(シミュレーション)
val userData = mapOf(
"id" to userId,
"name" to "ユーザー$userId",
"email" to "user$userId@example.com"
)
Timber.i("ユーザーデータ取得成功: %s", userData)
}
}
条件付きロギング
import timber.log.Timber
class ConditionalLoggingExample {
companion object {
// 条件付きログ出力のヘルパー関数
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("ネットワークリクエスト開始")
// 詳細ログは条件付きで出力
logNetworkInfo(enableVerboseLogging, "リクエストURL: https://api.example.com/users")
logNetworkInfo(enableVerboseLogging, "リクエストヘッダー: Authorization=Bearer xxx")
try {
// ネットワーク処理のシミュレーション
val response = simulateNetworkCall()
Timber.i("ネットワークリクエスト成功: ステータス=%d", response.statusCode)
logNetworkInfo(enableVerboseLogging, "レスポンスボディ: ${response.body}")
} catch (e: Exception) {
Timber.e(e, "ネットワークリクエスト失敗")
}
}
// デバッグビルドでのみ実行される重い処理
fun expensiveDebugOperation(data: Any) {
if (BuildConfig.DEBUG) {
val jsonData = convertToJson(data)
Timber.d("詳細データ:\n%s", jsonData)
}
}
private fun simulateNetworkCall(): NetworkResponse {
Thread.sleep(500) // ネットワーク遅延シミュレーション
return NetworkResponse(200, """{"users": [{"id": 1, "name": "田中太郎"}]}""")
}
private fun convertToJson(data: Any): String {
// JSON変換処理(重い処理のシミュレーション)
return """{"data": "$data", "timestamp": ${System.currentTimeMillis()}}"""
}
data class NetworkResponse(val statusCode: Int, val body: String)
}
// Fragment のライフサイクルでの条件付きログ
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 が表示開始")
}
override fun onResume() {
super.onResume()
logLifecycle("onResume", "Fragment がアクティブ")
}
override fun onPause() {
super.onPause()
logLifecycle("onPause", "Fragment が非アクティブ")
}
override fun onStop() {
super.onStop()
logLifecycle("onStop", "Fragment が非表示")
}
override fun onDestroy() {
super.onDestroy()
logLifecycle("onDestroy", "Fragment が破棄")
}
private fun logLifecycle(method: String, details: String) {
if (verboseLifecycleLogging) {
Timber.tag("Lifecycle").d("%s: %s", method, details)
} else {
Timber.d("Lifecycle: %s", method)
}
}
}
ログレベル設定
import timber.log.Timber
// カスタム Tree でログレベル制御
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 {
// カスタムタグフォーマット: クラス名.メソッド名:行番号
return "${element.className.substringAfterLast('.')}.${element.methodName}:${element.lineNumber}"
}
}
// ログレベル制御の使用例
class LogLevelExample {
fun demonstrateLogLevels() {
// 通常のログレベル設定
setLogLevel(Log.INFO)
Timber.v("Verbose ログ") // 出力されない
Timber.d("Debug ログ") // 出力されない
Timber.i("Info ログ") // 出力される
Timber.w("Warning ログ") // 出力される
Timber.e("Error ログ") // 出力される
// ログレベルを変更
setLogLevel(Log.DEBUG)
Timber.d("Debug ログ2") // 出力される
Timber.v("Verbose ログ2") // 出力されない
}
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("ログレベル設定: %s", levelName)
}
}
// タグ別ログレベル制御
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
}
}
// Application クラスでの詳細設定
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
// デバッグビルド: 詳細ログ
Timber.plant(FilteredDebugTree())
FilteredDebugTree.minimumLogLevel = Log.DEBUG
} else {
// リリースビルド: エラーログのみ
Timber.plant(ReleaseTree())
}
}
}
// リリース用 Tree (エラーログのみ)
class ReleaseTree : Timber.Tree() {
override fun isLoggable(tag: String?, priority: Int): Boolean {
// エラーと警告のみログ出力
return priority >= Log.WARN
}
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
if (isLoggable(tag, priority)) {
// Crashlytics やログ収集システムに送信
when (priority) {
Log.ERROR -> {
// エラーログをクラッシュレポートに送信
logErrorToCrashlytics(tag, message, t)
}
Log.WARN -> {
// 警告ログを分析システムに送信
logWarningToAnalytics(tag, message)
}
}
}
}
private fun logErrorToCrashlytics(tag: String?, message: String, throwable: Throwable?) {
// Crashlytics SDK を使用してエラーログ送信
// FirebaseCrashlytics.getInstance().recordException(throwable ?: Exception(message))
println("Crashlytics: [$tag] $message")
}
private fun logWarningToAnalytics(tag: String?, message: String) {
// Analytics SDK を使用して警告ログ送信
// Firebase Analytics や Google Analytics にイベント送信
println("Analytics: [$tag] $message")
}
}
Android固有の機能
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 固有のカスタム Tree 実装
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"
// 内部ストレージにログファイル保存
saveLogToFile(logLine)
// 例外情報がある場合は追加保存
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) {
// ファイル保存エラーの場合は標準ログに出力
Log.e("AndroidFileTree", "ログファイル保存エラー", e)
}
}
private fun getCurrentDate(): String {
return SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
}
}
// 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() {
// 既存の Tree をクリア
Timber.uprootAll()
if (BuildConfig.DEBUG) {
// デバッグビルド設定
Timber.plant(FilteredDebugTree().apply {
FilteredDebugTree.minimumLogLevel = logLevel
})
if (enableFileLogging) {
Timber.plant(AndroidFileTree(context))
}
} else {
// リリースビルド設定
Timber.plant(ReleaseTree())
}
Timber.i("Timber 設定完了: FileLogging=%s, LogLevel=%d, NetworkLogging=%s",
enableFileLogging, logLevel, enableNetworkLogging)
}
}
// Activity での Android 固有機能使用例
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 ライフサイクルログ
Timber.tag("ActivityLifecycle").i("MainActivity.onCreate")
// システム情報ログ
logSystemInfo()
// ネットワーク状態ログ
logNetworkState()
// アプリ情報ログ
logAppInfo()
}
private fun logSystemInfo() {
Timber.tag("SystemInfo").apply {
d("Android バージョン: ${android.os.Build.VERSION.RELEASE}")
d("API レベル: ${android.os.Build.VERSION.SDK_INT}")
d("デバイス: ${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}")
d("画面密度: ${resources.displayMetrics.density}")
d("利用可能メモリ: ${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("ネットワーク接続: %s", networkInfo.typeName)
} else {
w("ネットワーク未接続")
}
}
}
private fun logAppInfo() {
try {
val packageInfo = packageManager.getPackageInfo(packageName, 0)
Timber.tag("AppInfo").apply {
i("アプリバージョン: %s (%d)", packageInfo.versionName, packageInfo.versionCode)
i("パッケージ名: %s", packageName)
i("デバッグビルド: %s", BuildConfig.DEBUG)
}
} catch (e: Exception) {
Timber.e(e, "アプリ情報取得エラー")
}
}
private fun getAvailableMemory(): Long {
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val memoryInfo = ActivityManager.MemoryInfo()
activityManager.getMemoryInfo(memoryInfo)
return memoryInfo.availMem / (1024 * 1024) // MB に変換
}
}
パフォーマンス考慮事項
import timber.log.Timber
// パフォーマンス最適化のためのログ制御
class PerformanceOptimizedLogging {
companion object {
// 頻繁な呼び出しでのログ間引き
private var lastLogTime = 0L
private const val LOG_INTERVAL_MS = 1000L // 1秒間隔
fun logWithThrottling(tag: String, message: String) {
val currentTime = System.currentTimeMillis()
if (currentTime - lastLogTime >= LOG_INTERVAL_MS) {
Timber.tag(tag).d(message)
lastLogTime = currentTime
}
}
// 遅延ログ評価(重い処理がある場合)
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() {
// ✓ 良い例: シンプルな文字列ログ
Timber.d("処理開始")
// ✓ 良い例: 軽量な文字列テンプレート
val userId = 123
Timber.d("ユーザーID: %d", userId)
// ✗ 避けるべき例: 重い処理を直接含む
// Timber.d("重い処理結果: ${expensiveOperation()}")
// ✓ 良い例: 条件付きで重い処理実行
if (BuildConfig.DEBUG) {
val result = expensiveOperation()
Timber.d("重い処理結果: %s", result)
}
// ✓ 良い例: 遅延評価
logIfEnabled(Log.DEBUG, "Performance") {
"複雑な計算結果: ${complexCalculation()}"
}
// 大量データのパフォーマンステスト
performanceBenchmark()
}
private fun expensiveOperation(): String {
// 重い処理のシミュレーション
Thread.sleep(100)
return "処理完了"
}
private fun complexCalculation(): Double {
// 複雑な計算のシミュレーション
return (1..1000).map { Math.random() }.average()
}
private fun performanceBenchmark() {
val startTime = System.currentTimeMillis()
// 大量ログ出力のテスト
repeat(1000) { index ->
// ログの間引き
if (index % 100 == 0) {
Timber.d("進捗: %d/1000", index)
}
// 軽量な処理
processItem(index)
}
val duration = System.currentTimeMillis() - startTime
Timber.i("パフォーマンステスト完了: %dms", duration)
}
private fun processItem(index: Int) {
// アイテム処理のシミュレーション
if (index < 10) {
Timber.v("Item %d 処理", index)
}
}
}
// メモリ効率的なログ管理
class MemoryEfficientLogging {
private val logBuffer = mutableListOf<String>()
private val maxBufferSize = 100
fun addLog(message: String) {
logBuffer.add(message)
// バッファサイズ制限
if (logBuffer.size > maxBufferSize) {
logBuffer.removeAt(0) // 古いログを削除
}
}
fun flushLogs() {
if (logBuffer.isNotEmpty()) {
Timber.d("バッファログ出力: %d件", logBuffer.size)
logBuffer.forEach { Timber.d(it) }
logBuffer.clear()
}
}
// 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)
// 無効な参照をクリーンアップ
cleanupReferences()
}
private fun cleanupReferences() {
val iterator = contextReferences.iterator()
while (iterator.hasNext()) {
val entry = iterator.next()
if (entry.value.get() == null) {
iterator.remove()
}
}
}
}
// カスタム 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
// パフォーマンス統計収集
tag?.let {
logCounts[it] = logCounts.getOrDefault(it, 0) + 1
logTimes[it] = logTimes.getOrDefault(it, 0L) + duration
}
}
fun printStatistics() {
Timber.d("=== ログパフォーマンス統計 ===")
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)
}
}
}