読者です 読者をやめる 読者になる 読者になる

モバイルアプリのログ収集ライブラリ「Puree」をリリースしました

モバイルファースト室の @rejasupotaro です。

クックパッドでは、サービスをリリースしてログを収集して分析して改善してまたリリースして、というサイクルを素早く回すことでより良いものを作るということをウェブではやってきました。 クックパッドのサービス開発のフレームワークをモバイルアプリでも適用したいのですが、モバイルアプリにはウェブアプリと違ったロギングの難しさがあります。

今回はモバイルアプリのロギングの問題点とPureeというログ収集ライブラリについて話します。

モバイルアプリのロギングの難しさ

ウェブアプリでは、基本的にはサーバー側でログを収集することができますが、モバイルアプリの場合は画面の制御はアプリ側で行われ、APIを介してデータを受け取るため、クライアント側でログを収集して送信する必要があります。

f:id:rejasupotaro:20141123225718p:plain

アプリのログを収集するのに、画面遷移をしたりタップするたびにサーバーにログを送るということも考えられますが、パフォーマンスやデータ使用量やバッテリーのことを考えるとあまり現実的ではありません。

f:id:rejasupotaro:20141123224948p:plain

また、モバイルアプリは動的に接続状況が変わるので送信に失敗したログをケアしなければなりませんし、そのためにはログをバッファリングする必要があります。

f:id:rejasupotaro:20141123225027p:plain

そこでクックパッドではPureeというライブラリを作って、モバイルアプリからはPureeを介してログを送信しています。

ログ収集ライブラリPureeについて

Pureeは以下の機能を備えています。

  • フィルタリング: 共通のパラメータを付与したり、サンプリングを行ったりすることができます。
  • バッファリング: ログを一時的に貯めて、送信に失敗したときや端末の再起動などでログが消えないようにする役割を持っています。
  • バッチ: 複数のログを一つにまとめて送ることができます。
  • リトライ: ログの送信に失敗した場合は、一定時間経過後に自動で再送信を試みます。

f:id:rejasupotaro:20141123224958p:plain

また、Pureeはアウトプットプラグインという形でログの送信先の切り替えたり、フィルターを適用したりすることができます。

f:id:rejasupotaro:20141123232523p:plain

これらの実装は Fluentd を参考にしています。

Pureeの使い方

ログを送る

例として、タップのログを定義します。 ログはJsonConvertibleクラスを継承します。

public class ClickLog extends JsonConvertible {
    @SerializedName("page") private String page;
    @SerializedName("label") private String label;

    public ClickLog(String page, String label) {
        this.page = page;
        this.label = label;
    }
}

ログを定義したら任意の箇所で Puree.send メソッドにログと送り先を渡します。

Puree.send(new ClickLog("MainActivity", "track"), OutLogcat.TYPE);

アウトプットプラグインを定義する

アウトプットプラグインにはPureeOutputとPureeBufferedOutputの二種類があります。 PureeOutputはバッファリングは行わず、ただちにログの出力を行います。Google AnalyticsやMixpanelのようなライブラリ内にバッファのしくみがあるようなものに出力する場合はこちらを使います。

例えば、ログをLogcatに出力するプラグインは下のようになります。

public class OutLogcat extends PureeOutput {
    public static final String TYPE = "logcat";

    @Override
    public String type() {
        return TYPE;
    }

    @Override
    public OutputConfiguration configure(OutputConfiguration conf) {
        return conf;
    }

    @Override
    public void emit(JSONObject jsonLog) {
        Log.d(TYPE, jsonLog.toString());
    }
}

PureeBufferedOutputを継承すると、バッファリングとリトライを自動で行うようになります。 デフォルトのインターバルは2分になっていて、現状ではログの送信に失敗すると baseInterval * (retryCount + 1) 後に再送信を試みます。また、デフォルトの最大リトライ回数は5回に設定されていて、5回連続で失敗した場合は次回のイベント発火時に送られます。 一度に送るログの量も制限することができます。

public class OutBufferedLogcat extends PureeBufferedOutput {
    public static final String TYPE = "buffered_logcat";

    @Override
    public String type() {
        return TYPE;
    }

    @Override
    public OutputConfiguration configure(OutputConfiguration conf) {
        return conf;
    }

    @Override
    public void emit(JSONArray jsonLogs, AsyncResult asyncResult) {
        Log.d(TYPE, jsonLogs.toString());
        asyncResult.success();
    }
}

emitメソッドに出力するログが入ってくるので、そこでGoogle AnalyticsやMixpanelに送ったり、APIを叩いたりします。 実際にPureeBufferedOutputを使う場合には非同期で結果を受け取ることになると思うので、ログの送信に成功したか失敗したかを知らせるためにコールバックで asyncResult#successasyncResult#fail を呼ぶ必要があります。

@Override
public void emit(JSONArray jsonLogs, final AsyncResult asyncResult) {
    client.sendLogs(jsonLogs, new Callback() {
        @Override
        public void success() {
            asyncResult.success();
        }

        @Override
        public void fail() {
            asyncResult.fail();
        }
    });
}

フィルターの定義

Pureeのsendが呼ばれたあとに、対応したフィルターのapplyメソッドが呼ばれます。 そこでログに共通するパラメータを付けたり、特定の条件のときには送らないようにするなどができます。 たとえばイベントの起こった時間を付与するフィルターは下のようになります。

public class AddEventTimeFilter implements PureeFilter {
    public JSONObject apply(JSONObject jsonLog) throws JSONException {
        jsonLog.put("event_time", DateUtils.getTimestamp());
        return jsonLog;
    }
}

Pureeの初期化

Pureeでログを送るには前もってプラグインを登録する必要があります。 初期化はApplicationクラスの中などで行います。

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        Puree.initialize(buildConf(context));
    }

    public static PureeConfiguration buildConf(Context context) {
        PureeFilter addEventTimeFilter = new AddEventTimeFilter();
        return new PureeConfiguration.Builder(context)
                .registerOutput(new OutLogcat(), addEventTimeFilter)
                .registerOutput(new OutBufferedLogcat(), addEventTimeFilter)
                .registerOutput(new OutDisplay(), new SamplingFilter(0.5F))
                .registerOutput(new OutBufferedDisplay())
                .build();
    }

第一引数にアウトプットプラグインを渡して、第二引数以降にオプションでフィルターを渡します。

Pureeの導入方法

Pureeはjcenterで公開していますので、build.gradleのrepositoriesにjcenterを追加した上で、dependenciesにpureeを追加してください。

// build.gradle
buildscript {
    repositories {
        jcenter()
    }
    ...

// app/build.gradle
compile 'com.cookpad:puree:1.0.0'

ソースコードは GitHub で公開しています。

まとめ

Pureeを作る前はログの出力先が増えたり、プロジェクトが変わったりするたびに独自にログを収集するしくみを作っていました。 ログはサービスを成長させるためには重要です。Pureeを導入することでログを収集するしくみを実装する時間を短縮して、その分「どのログを取るか」「どうやってログを活かすか」を考える時間に使えればいいなと思います。

Pureeは今のところはAndroid版だけを公開していますが、iOS版の開発も進んでいるので、近いうちに公開されると思います。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/