はじめに
リリースしたモバイルアプリで、未検出のバグによりクラッシュしてしまう場合、ユーザーが声を上げない限り検知できない・・ということは避けたいですよね。
また、万が一そのような不具合があった場合、再現手順やユーザーの状態を調査・分析するのは骨が折れます。
更には、リリースしたモバイルアプリの動作ログを自前のプラットフォームに集約するような仕組みも敷居が高いです。
それらを踏まえて、
- クラッシュするまでの詳細なログをクラッシュレポートで確認したい
 - ログはいい感じに出力したい(
if (Logger.isDebugEnabled()) { ... }みたいな分岐はイヤ) - 手軽に導入したい
 
という思いのもと、Timber と Firebase Crashlytics での実装例を紹介したいと思います。
Timber
Timber は、Android向けのログ出力ライブラリです。
ログと言えば、Androidの標準ロガーとして、android.util.Logを使用することが多いと思います。
Androidの標準ロガー
しかし、下記の記事で触れられていますが、標準のロガーを使用していると以下の問題が出てきます。
- クラスごとにログ出力時のタグ用の変数を用意する必要がある
- 
private final String TAG = MainActivity.class.getName();と冒頭に宣言 
 - 
 - リリースビルドしたアプリの logcat にデバッグログが出力されてしまう
- ProGuard 等で削除していれば問題はないが、機密情報を出力していたとしたら大変・・・
 
 - デバッグビルドなのに Crashlytics へのクラッシュレポートにログが出力されてしまう
- ログごとに “リリースビルドなら“ みたいな分岐はしたくない・・・
 
 
Timber を使うメリット
各問題に対して、Timber を使用することでのメリットを挙げます。
・クラスごとにログ出力時のタグ用の変数を用意する必要がある
呼び出し元クラス名が自動でタグに設定されるため、変数の用意や引数への指定が不要になります。
// 導入前
private final String TAG = MainActivity.class.getName();
...
Log.d(TAG, String.format("昨日のご飯は%s鍋!", "トマト"));
// 出力例
// D/com.example.timbercrashlyticstest.MainActivity: 昨日のご飯はトマト鍋!
// 導入後
Timber.d("昨日のご飯は%s鍋!", "トマト");
// 出力例
// D/MainActivity: 昨日のご飯はトマト鍋!
・リリースビルドしたアプリの logcat にデバッグログが出力されてしまう
・デバッグビルドなのに Crashlytics へのクラッシュレポートに出力されてしまう
リリースビルド/デバッグビルドによって、ログ出力を切り替えできるため、容易に制御が可能になります。
※ 後述します
導入方法
build.gradle(:app)
TimberのGitHub を参考にモジュールを追加します。
dependencies {
+  implementation 'com.jakewharton.timber:timber:4.7.1'
}
※ 執筆時点(2021/11/18)での最新は 5.0.1 ですが、筆者の環境の都合上バージョンを落としています
MyApplication
Tree クラスを plant することで、アプリケーション全体で Timber クラスを使用できます。
import android.app.Application;
+ import timber.log.Timber;
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
+        Timber.plant(new Timber.DebugTree());
    }
}
AndroidManifest.xmla
manifest/application/android:name に、アプリケーションクラスを指定
    <application
    	...
+        android:name=".MyApplication"
        ...>
MainActivity
public class MainActivity extends AppCompatActivity {
    private final String TAG = MainActivity.class.getName() + "_TAG";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.v(TAG, "Log VERBOSE");
        Log.d(TAG, "Log DEBUG");
        Log.i(TAG, "Log INFO");
        Log.w(TAG, "Log WARN");
        Log.e(TAG, "Log ERROR");
        Log.println(Log.ASSERT, TAG, "Log ASSERT");
        Timber.v("Timber VERBOSE");
        Timber.d("Timber DEBUG");
        Timber.i("Timber INFO");
        Timber.w("Timber WARN");
        Timber.e("Timber ERROR");
        Timber.wtf("Timber ASSERT");
    }
}
出力例
Timber の方はパッケージ名が出力されないのですっきりしています。
パッケージ名が必要であれば、お好みで TAG 変数をタグとして設定してください。
※ Crashlytics を使用する場合、パッケージ名が冗長となり得るなら除く方針をお勧めします
Logcat
Runウィンドウ
ビルドによる切り替え
下記のように、ビルドによって plant するクラスを切り替えることで、logcat に出力するログを優先度ごとに制御することが可能になります。
つまり、MainActivity では、ビルドモードを意識して分岐するといった処理が必要なくなります。
※ Tree.DebugTree は全優先度のログを出力します
MyApplication
- 
BuildConfig.DEBUGでビルド別に分岐します。 - デバッグビルドの場合は 
DebugTree、リリースビルドの場合はReleaseTreeをplantします。 
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
+        if (BuildConfig.DEBUG) {
            Timber.plant(new Timber.DebugTree());
+        } else {
+            Timber.plant(new ReleaseTree());
+        }
    }
}
ReleaseTree
Timber.Tree を継承したリリースビルド用の Tree クラスを作成します。
下記は実装の一例です。
public class ReleaseTree extends Timber.Tree {
    @Override
    protected void log(int priority, @Nullable String tag, @NotNull String message, @Nullable Throwable t) {
        switch (priority) {
            case Log.INFO:
            case Log.WARN:
            case Log.ERROR:
            case Log.ASSERT:
                // Crashlytics ログ出力処理を記述(Crashlytics編で記載)
                break;
            default:
                /*
                下記の優先度は出力なし
                ・Log.VERBOSE
                ・Log.DEBUG
                 */
                break;
        }
    }
}
おまけ
Lint
Timber を追加すると Lint も設定されるため、既に Log クラスで実装してしまった場合でも、Log クラスで実装しようとしたときも、気付けるようになっていて便利です。
関数名の出力
Timber.Tree ではなく、Timber.DebugTree を継承し、createStackElementTag をオーバーライドすることで、呼び出し元の関数名を出力するようにカスタマイズが可能です。
public class OmakeDebugTree extends Timber.DebugTree {
    @Override
    protected @Nullable String createStackElementTag(@NotNull StackTraceElement element) {
        // ClassName#MethodName()
        return String.format(
                "%s#%s",
                super.createStackElementTag(element),
                element.getMethodName()
        );
    }
}
※ OmakeDebugTree を plant してください
出力例(Runウィンドウ)
おわりに
これで、デバッグビルドの際はコンソール上にログを出力し、リリースビルドの際はクラッシュレポートに必要なログのみを出力する基盤ができました。
クラッシュレポートを送信するサービスは様々なものがありますので、応用する際に参考になれば幸いです!
次回は Firebase Crashlytics を ReleaseTree に組み込んでいきたいと思います。



