TimberとRealtime Databaseを併用してエラーログを収集する

この記事は「[Android Advent Calendar 2017](https://qiita.com/advent-calendar/2017/android)」１日目の記事となります。

みなさんはAndroidアプリでFirebaseプロジェクトを活用していますか？
多くのプロジェクトはStabilityカテゴリ、Analyticsカテゴリ、Growカテゴリの機能は使っているものの、Developカテゴリの機能にはほとんど手をつけていないのではないでしょうか？
ちなみにDevelopカテゴリの機能はAuthenticationやRealtime Database、Storageなどがあります。

特にRealtime DatabaseはRelational Database（RDB）ではないため既存のDBを移行する難易度も高く、そこまでするほどのメリットがないことも多いです。
かといって新規で利用するとしても、不特定多数のユーザーへ向けた機能として使うことを考えると、無料プランの制限数が微妙に悩ましいです。

ですが、せっかくの高機能なRealtime Databaseを放置するのはもったいない。
なのでDevelopカテゴリらしく、完全に開発用途で利用してしまおう、というのがこの記事の主旨となります。

## この記事の前提

 * Timberが何かを知っている
 * アプリへAnalyticsやCrashReportingといった実装のやり方を知っている

Timberとは、例のJake Wharton氏が開発した、loggerライブラリです。
Timber自体の紹介は、下のスライド資料がとてもわかりやすいので一読してもらえれば、どんなものかは大体理解してもらえるかと思います。

「[Androidのログ出力をいい感じにする #potatotips 9 // Speaker Deck](https://speakerdeck.com/cutmail/androidfalseroguchu-li-woiigan-zinisuru-number-potatotips-9)」

## 作るシステムの概要

今回はTimberによってログ出力する際に、一部のログをRealtime Databaseに登録するというシステムを実装します。
Realtime Databaseにデータをストックすれば、それをトリガにCloud Functionsを実行できます。
これを利用すれば、たとえば検証期間中のエラーログをすばやく発見し、リリース前の重大なバグを発見できるという使い方も可能です。（ちなみに今回、Cloud Functions周りの解説は省きます）

![system_image.png](https://qiita-image-store.s3.amazonaws.com/0/4560/4da20dba-dded-858f-89f6-0960657b3043.png)

## 実装

それでは実装の解説です。
今回は簡単に、

 * 発生時刻
 * 端末のデバイス名
 * 端末のモデル名
 * 端末のOSバージョン
 * アプリのバージョン
 * アプリのビルドタイプ
 * エラーメッセージ

といった情報を送信することにします。

### Realtime Databaseのルール設定について

Realtime Databaseでは読み書きの権限をjsonファイルで指定します。
通常はAuthenticationとの組み合わせで、ログインユーザー以外は書き込み不能といった設定をします。
ただ今回は非ログイン状態でエラーログを送信する必要があるので、ログインユーザー以外でも書き込み可能な設定に変えておきましょう。

```json:database.rules.json
{
  "rules": {
    ".read": "false",
    ".write": "true"
  }
}
```

今回はエラーログ収集用途以外でRealtime Databaseを使わない前提としているため、データベース全体に対して設定しています。
いちおうAdmin権限以外でログ一覧を見ることができないようにするため、readはfalseとしています。

### エラーログ送信処理の実装

それでは実装部分の解説です。
まずはエラー情報を送るためのデータクラスを用意します。

```kotlin:ErrorLog.kt
data class ErrorLog(
        val timestamp: Long,
        val date: String,
        val device: String,
        val model: String,
        val version: String,
        val appVersion: String,
        val buildConfig: String,
        val message: String
)
```

ただのデータクラスですので、解説することはほとんどありません。
ただ、 `@IgnoreExtraProperties` アノテーションはつけなくても、送信には問題ありませんでした。

### `Timber.Tree`クラスに送信処理を追加する

次は送信処理です。
今回は `Timber.e()` が呼び出されたタイミングでエラー情報を送る形で実装しています。

```kotlin:CustomApplication.kt
private class ReleaseTree : Timber.Tree() {
   override fun log(priority: Int, tag: String?, message: String, throwable: Throwable?) {
        if (priority != Log.ERROR) {
            return
        }

        // TimeStampをkeyにして、エラーログを送信する
        val timestamp = System.currentTimeMillis()
        val df = SimpleDateFormat("yyyy-MM-dd HH:mm:ssZ", Locale.JAPAN)
        df.timeZone = TimeZone.getTimeZone("Asia/Tokyo")

        val errorLog = ErrorLog(
                timestamp,
                df.format(Date()),
                Build.DEVICE,
                Build.MODEL,
                "%s(%d)".format(Build.VERSION.RELEASE, Build.VERSION.SDK_INT),
                BuildConfig.VERSION_NAME,
                BuildConfig.BUILD_TYPE,
                message
        )
        val reference = FirebaseDatabase.getInstance().getReference("/errorLogs")
        reference.child("%d".format(timestamp)).setValue(errorLog)
   }
}
```


あとはApplicationクラスの onCreate() で、作成したカスタムTreeクラスを利用するように設定すれば完了です。
サンプルコードではリリース版でも利用する呼び出し方になっています。
たとえばリリース前検証だけで利用したいのであれば、ビルドタイプやフレーバー別で切り分けるようにするのもありでしょう。

```kotlin:CustomApplication.kt
class RecpoApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        // ...

        if (BuildConfig.DEBUG) {
            Timber.plant(Timber.DebugTree())
        } else {
            Timber.plant(ReleaseTree())
        }
    }
}
```

これでエラーが発生するたび、Realtime Databaseの`errorLogs`以下にデータがストックされるようになりました。

![stack_errorlog.png](https://qiita-image-store.s3.amazonaws.com/0/4560/cc6556cf-46a9-db2d-f0c3-5c51c1795aac.png)

## 実装時の注意

残念ながらこの実装だけの場合、アプリが落ちる（クラッシュ）レベルのエラーまで対応できていません。
Firebaseへ完全に取り込まれて以降のCrashlyticsやCrashReportingであれば、Cloud Functionsを使うことで対応できるかもしれません。
まだそのあたりは未調査です…。

## 収集したログの使い道

これでエラーログ自体を収集できました。
あとはCloud Functionsを使うことで、「エラーログがストックされたらSlackへ通知する」というしくみを作ることもできます。
ですが、Cloud Functionsは無料プランだとGoogle外のサービスを利用できません。

GAEを介することで、（無料プランのまま）外部サービスを利用することも可能らしいです（自分は試していない）。
他にもあるものをひとつ挟むことで、同様に無料プランのまま、外部サービスを利用（今回はSlackのAPI）できます。
こちらは「[Android Things AdventCalendar](https://qiita.com/advent-calendar/2017/androidthing)」で紹介します。

## まとめ

現在ほぼ上位互換のFireStoreという機能もリリースされて、さらに使いみちがなくなりそうなRealtime Database。
放置するぐらいなら「とりあえず生！」という感じで、こんな風に使うのもありではないかと思います。