Unyablog.

のにれんのブログ

Mackerel Agent for Android

この記事は Mackerel アドベントカレンダー 21 日目の記事です。

qiita.com

Mackerel について

Mackerel とはサーバーを監視するサービスで、Slack などとの連携が便利だったりグラフが美しかったりと良いサービスです。

Mackerel にデータを送るためのサーバー監視ソフトウェアとしては mackerel-agent というものがあり、これをインストールするとサーバー上でメトリックを定期的に取得し、送信してくれます。

github.com

本題

今回はその Android 版を作ってみました。

Mackerel Agent for Android です。

f:id:nonylene:20161221015614p:plain

昨日から PlayStore に公開しています!

f:id:nonylene:20161222000441p:plain:w150

play.google.com

GitHub はこちらです。

github.com

対応バージョンは Android 4.4 以上です。

機能

1分毎にメトリックを集め、5分毎にサーバーに送信します。Mackrel には 1 ホストとして登録するようにしています。

残念ながら mackerel-plugin は使えませんが、公式の mackerel-agent が現在送信しているメトリックと spec は全てカバーしています。

動機

実装すると proc が学べるし良いかなと思ったからです。あとバッテリー切れそうな時に通知してくれると便利かもと思いました(がまだ Android 関係のカスタムメトリックは実装していない…)。

実装等

AndroidLinux なので概ね mackrel-agent の Linux 版と同じ処理でメトリックを取得できます。

実装は Kotlin で行いました。アプリの特性上 proc ファイルをたくさん読むのですが、Kotlin だとファイル関係がかなり楽なので良かったです。

// Interface の Spec を読む例
File("/proc/net/dev").readLines()
        .map { it.split(":").map(String::trim) }
        .filter { it.size == 2 }
        // remove loopback
        .filter { it[0] != "lo" }
        .mapNotNull {
            val values = it[1].split(Regex("\\s+")).map(String::toDouble)
                // all zero -> remove data
            if (values.all { it == 0.0 }) return@mapNotNull null
            InterfaceStat(it[0].trim(),
                    values[0], values[1], values[2], values[3], values[4], values[5], values[6],
                    values[7], values[8], values[9], values[10], values[11], values[12], values[13],
                    values[14], values[15], time
            )
        }

ログや取得した値のキャッシュは Realm を使っています。proc を用いて差分を出すには数秒間待つなどの必要がありますが、そのあたりは RxJava を用いていい感じにしました。*1

Android の辛いところ

コマンド

mackerel-agent ではいくつかコマンドを打って結果をパースしている物があります。

dfuname がそれに当たるのですが、これを Android で打とうとするとバージョン問題に直面します。

Android 5.0 未満では df は一切オプション受け付けない上に独自の形式で出力され、uname においては存在すらいしていません。

結局 df では以下のようにバージョンで分けるようにして、uname は proc から取得できる値を取得することにしました。*2

MackerelAgentAndroid/FileSystem.kt at 48a67bdd9387ec519800612e847ea28bc4bacf84 · nonylene/MackerelAgentAndroid · GitHub

Android 6.0 以降では toybox のお陰でコマンド環境は向上し、大抵の必要なコマンドは使えるようになりました。

詳しくは

nonylene.hatenablog.jp

に書いています。

定期実行

Android で定期的にタスクを実行するにはいくつかの方法があります。このアプリはフォアグラウンドサービスとして常駐するようにしていますが、代表的なものは AlarmManager でしょう。

しかし、今回は Forground Service を使いました。なので通知バーに常駐しています。

これは Android 6.0 からのバッテリー節約のための新機能である Doze / AppStandby によるのものです。

developer.android.com

doze で1分間毎に実行するのが崩れる*3のは構わないのですが、問題は App Standby です。

AppStandby は、充電していない状態で一定の条件とネットワークの使用が 一日に一回 に制限されるというものです。

この "一定の条件" とは

  • The user explicitly launches the app.

  • The app has a process currently in the foreground (either as an activity or foreground service, or in use by another activity or foreground service).

  • The app generates a notification that users see on the lock screen or in the notification tray.

Optimizing for Doze and App Standby | Android Developers

となっています。

裏で毎分取得して API に送信するようなアプリはわざわざ立ち上げることも無いでしょうし、通知も出すことも無いので、バックグラウンドで走らせているとこの条件に当てはまるのは必須です。せめて端末が点いているときは送信して欲しいので、あまり推奨はされていませんが Forground Service を用いています。

Forground Service を使うと電池持ちが悪くなるように思えますが、 Forground Service で定期実行してもスリープ中は動作は遅れているので影響はほとんどありませんでした。

また、稀に CPU の使用率が -100 % になったりするみたいです。ちゃんと見ていませんが、これは CPU 使用量などの値が定期的にリセットされることによるものだと思います。結局マイナスの場合は例外を投げることにしました。

まとめ

proc を読んで Kotlin で mackerel-agent を実装していくのは楽しかったです。送信したデータを見るのも、アプリを使っている時間にメモリや CPU が使われているのが分かったりして面白いです。ぜひ入れてみてください。PR などもご気軽にどうぞ!

*1:ちなみに RxJava を Kotlin から使うと時々 SAM が辛くなったりする… https://youtrack.jetbrains.com/issue/KT-14754

*2:NDK を入れて C から System Call を呼べば取得できる気がしますが、今回は行っていません。

*3:システムがしばらく未使用状態となり Doze に入ると AlarmManager の定期実行が遅延して、許可された時間のみに行われるようになる