はじめに
Androidアプリ開発においてログ出力は、デバッグや動作確認に欠かせない作業です。標準の android.util.Log クラスを使うことが多いですが、実務では Timber というライブラリを採用するケースが増えています。
本記事では、Log と Timber の違いを整理し、なぜ Timber が現場で重宝されるのかを説明します。
android.util.Log とは
android.util.Log は Android が標準で提供するログクラスです。以下のレベルでログを出力できます。
| メソッド | 用途 | 動作 |
|---|---|---|
Log.v() |
Verbose | 開発時のみコンパイル、基本的に使わない |
Log.d() |
Debug | コンパイルはされるがランタイムで除去される |
Log.i() |
Info | 常に保持される |
Log.w() |
Warning | 常に保持される |
Log.e() |
Error | 常に保持される |
参考: Android公式 - Understand logging
公式ドキュメントによると、DEBUG と VERBOSE ログは リリースビルドでは除去される 設計になっていますが、Log クラスを素のまま使うと リリースビルドでも Log.d() のコードが残り続けます。
Log の問題点:ラッパーが必要になる
Log クラスをそのまま使うと、以下のような問題が発生します。
1. リリースビルドでもログが出力される可能性がある
Log.d() はコンパイルされたコードに残るため、意図せずデバッグログがリリースアプリに含まれてしまうことがあります。個人情報や機密情報が含まれていると、セキュリティリスクになります。
2. ビルドタイプごとに出し分けができない
Debug ビルドではすべてのログを出力し、Release ビルドでは ERROR のみにしたい、という要件は現場でよくあります。これを Log クラスで実現しようとすると、ラッパークラスを自前で実装する必要があります。
// よくある自前ラッパーの例
object AppLogger {
private val isDebug = BuildConfig.DEBUG
fun d(tag: String, message: String) {
if (isDebug) {
Log.d(tag, message)
}
}
fun e(tag: String, message: String) {
Log.e(tag, message)
}
fun e(tag: String, message: String, throwable: Throwable) {
Log.e(tag, message, throwable)
}
}
毎プロジェクトでこういったラッパーを書くのは手間であり、品質もまちまちになりがちです。
Timber とは
Timber は Jake Wharton 氏が作成したAndroid向けのログライブラリです。
Log クラスのラッパーとして機能し、以下の特徴があります。
- TAG を自動で設定してくれる(クラス名が自動で TAG になる)
- DebugTree / ReleaseTree の仕組みで Debug/Release の出し分けが簡単
- シンプルな API で Log クラスとほぼ同じ感覚で使える
- カスタム Tree を実装することで、Crashlytics などへの連携も容易
セットアップ
// build.gradle.kts
dependencies {
implementation("com.jakewharton.timber:timber:5.0.1")
}
初期化(Application クラス)
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
// Debugビルドのみ全ログを出力
Timber.plant(Timber.DebugTree())
} else {
// Releaseビルドではエラーのみ出力(例: Crashlyticsへ送信)
Timber.plant(ReleaseTree())
}
}
}
// Releaseビルド用のカスタムTree
class ReleaseTree : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
if (priority == Log.ERROR || priority == Log.WARN) {
// Crashlytics などへ送信する処理
// FirebaseCrashlytics.getInstance().log(message)
}
}
}
Log と Timber の実装比較
実際のログ出力コードを比較してみましょう。
Log を使った場合
class UserRepository {
companion object {
private const val TAG = "UserRepository"
}
fun fetchUser(userId: String) {
Log.d(TAG, "fetchUser called: userId=$userId")
try {
// ... 処理 ...
} catch (e: Exception) {
Log.e(TAG, "fetchUser failed", e)
}
}
}
Timber を使った場合
class UserRepository {
fun fetchUser(userId: String) {
Timber.d("fetchUser called: userId=%s", userId)
try {
// ... 処理 ...
} catch (e: Exception) {
Timber.e(e, "fetchUser failed")
}
}
}
TAG の定義が不要になり、コードがスッキリします。
重要な違い:Debug ビルドでのログ出力挙動
ここが Log と Timber の大きな違いです。
Log.e() の場合
// ⚠️ Log.e() はビルドタイプに関わらず常に出力される
Log.e(TAG, "エラーが発生しました")
// → Debug ビルド: 出力される ✅
// → Release ビルド: 出力される ⚠️(意図しないログが残る可能性)
Log クラスのレベル別挙動をまとめると、
-
Log.v()/Log.d()→ ランタイムでは除去されることが期待されるが、実際はコードに残る -
Log.i()/Log.w()/Log.e()→ 常に保持される
Timber.e() の場合
// ✅ Timber.e() はビルドタイプによって挙動を制御できる
Timber.e("エラーが発生しました")
// → Debug ビルド(DebugTree あり): 出力される ✅
// → Release ビルド(ReleaseTree のみ): Tree の実装次第で制御可能 ✅
Application クラスで Debug ビルドの時のみ DebugTree を plant することで、Debug ビルドでは全ログが出力され、Release ビルドでは出力しない(または ERROR のみ) という制御が簡単に実現できます。
// Debug ビルドのみ全ログ出力、それ以外は何もしない例
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
// → Debug: Timber.d(), Timber.i(), Timber.e() すべて出力
// → Release: どの Timber.x() も出力されない(plantしていないため)
まとめ
| 比較項目 | Log | Timber |
|---|---|---|
| TAG の定義 | 自前で定義が必要 | 自動(クラス名) |
| Debug/Release の出し分け | ラッパー実装が必要 |
plant() で簡単に制御 |
e() の Release での出力 |
常に出力される | Tree の実装次第で制御可能 |
| セットアップコスト | 不要 | Application で初期化が必要 |
| 外部サービス連携 | 自前実装が必要 | カスタム Tree で容易に実装可能 |
Log クラスは手軽に使える一方、実務レベルでは ビルドタイプごとのログ制御 や セキュリティ上の理由 から、ラッパーの実装が必須になります。Timber を使えば、そういった課題をシンプルに解決できるため、新規プロジェクトでは積極的に採用を検討してみてください。