Timber×Crashlytics でログをクラッシュレポート送信（Timber編）


# はじめに

リリースしたモバイルアプリで、未検出のバグによりクラッシュしてしまう場合、ユーザーが声を上げない限り検知できない・・ということは避けたいですよね。

また、万が一そのような不具合があった場合、再現手順やユーザーの状態を調査・分析するのは骨が折れます。

更には、リリースしたモバイルアプリの動作ログを自前のプラットフォームに集約するような仕組みも敷居が高いです。



それらを踏まえて、

- クラッシュするまでの詳細なログをクラッシュレポートで確認したい
- ログはいい感じに出力したい（`if (Logger.isDebugEnabled()) { ... }`みたいな分岐はイヤ）
- 手軽に導入したい

という思いのもと、Timber と Firebase Crashlytics での実装例を紹介したいと思います。



# Timber

[Timber](https://github.com/JakeWharton/timber) は、Android向けのログ出力ライブラリです。

ログと言えば、Androidの標準ロガーとして、`android.util.Log`を使用することが多いと思います。



## Androidの標準ロガー

しかし、下記の記事で触れられていますが、標準のロガーを使用していると以下の問題が出てきます。



https://qiita.com/omooooori/items/aaa1cc15c3ca16e923f8



- クラスごとにログ出力時のタグ用の変数を用意する必要がある
  - `private final String TAG = MainActivity.class.getName();` と冒頭に宣言
- リリースビルドしたアプリの logcat にデバッグログが出力されてしまう
  - ProGuard 等で削除していれば問題はないが、機密情報を出力していたとしたら大変・・・
- デバッグビルドなのに Crashlytics へのクラッシュレポートにログが出力されてしまう
  - ログごとに “リリースビルドなら“ みたいな分岐はしたくない・・・



## Timber を使うメリット

各問題に対して、Timber を使用することでのメリットを挙げます。

> ・クラスごとにログ出力時のタグ用の変数を用意する必要がある



呼び出し元クラス名が自動でタグに設定されるため、変数の用意や引数への指定が不要になります。



```java
// 導入前
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](https://github.com/JakeWharton/timber#download) を参考にモジュールを追加します。



```diff_gradle:builde.grade(app)
dependencies {
+  implementation 'com.jakewharton.timber:timber:4.7.1'
}
```

※ 執筆時点（2021/11/18）での最新は `5.0.1` ですが、筆者の環境の都合上バージョンを落としています



### MyApplication

`Tree` クラスを `plant` することで、アプリケーション全体で `Timber` クラスを使用できます。

```diff_java:MyApplication.java
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` に、アプリケーションクラスを指定

```diff_xml:AndroidManifest.xml
    <application
    	...
+        android:name=".MyApplication"
        ...>
```



### MainActivity

```java:MainActivity.java
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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1215832/fed3f335-00d2-bc85-4ec8-2c0a0ff38eac.png)



##### Runウィンドウ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1215832/641a8b0e-daf6-0ffa-0c29-cc54921d0ca0.png)



## ビルドによる切り替え

下記のように、ビルドによって `plant` するクラスを切り替えることで、logcat に出力するログを優先度ごとに制御することが可能になります。

つまり、`MainActivity` では、ビルドモードを意識して分岐するといった処理が必要なくなります。
※ `Tree.DebugTree` は全優先度のログを出力します



### MyApplication

- `BuildConfig.DEBUG` でビルド別に分岐します。
- デバッグビルドの場合は `DebugTree`、リリースビルドの場合は `ReleaseTree` を `plant` します。



```diff_java:MyApplication.java
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` クラスを作成します。
下記は実装の一例です。



```java:ReleaseTree.java
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` クラスで実装しようとしたときも、気付けるようになっていて便利です。



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1215832/ace8a9c7-beca-08e8-1fa8-7cc9204e9687.png)



### 関数名の出力

`Timber.Tree` ではなく、`Timber.DebugTree` を継承し、`createStackElementTag` をオーバーライドすることで、呼び出し元の関数名を出力するようにカスタマイズが可能です。



```java:OmakeDebugTree.java
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ウィンドウ）
`#`のあとに、関数名が出力されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1215832/bb925b9e-74a6-d95f-9f7f-6946871ab849.png)



# おわりに

これで、デバッグビルドの際はコンソール上にログを出力し、リリースビルドの際はクラッシュレポートに必要なログのみを出力する基盤ができました。

クラッシュレポートを送信するサービスは様々なものがありますので、応用する際に参考になれば幸いです！

次回は Firebase Crashlytics を `ReleaseTree` に組み込んでいきたいと思います。



## 参考

- [Androidのログ出力ライブラリ”Timber”](https://qiita.com/omooooori/items/aaa1cc15c3ca16e923f8)
- [ProGuardでデバッグログを"完全"に削除することの難しさ](https://tech.prog-8.com/entry/2021/03/25/080000)