ログ収集ライブラリ Puree の iOS 版をリリースしました

モバイルファースト室の @slightair です。 先日、モバイルアプリのログ収集ライブラリ「Puree」をリリースしました という記事で Puree というログ収集ライブラリを紹介しました。 Android 版につづき iOS 版もリリースしたので紹介したいと思います。

puree-ios : https://github.com/cookpad/puree-ios

モバイルアプリのログ記録の難しさや、それを解決するための Puree の思想についての説明は前回の記事におまかせします。 iOS 版の Puree も Android 版と同じようにフィルタリング、バッファリング、バッチ、リトライの機能を備えています。 ログのフィルタリングや出力の振る舞いをプラグインとして定義し、それらを組み合わせることで効率的なログ収集を実現します。

Puree iOS の使い方

Puree の導入方法

Puree-iOS は CocoaPods でプロジェクトに導入することができます。 Podfile に以下のように書いてください。

pod "Puree"

Puree の初期化

PURLogger クラスのインタンスを生成し、このロガーにログをポストしていくのが基本的な Puree の使い方です。 PURLoggerConfiguration クラスのインスタンスでフィルタやアウトプットなどのプラグインの設定を行い、ロガーの作成時に渡します。 プラグインの設定にはプラグインのクラスとその設定、反応するログのタグと一致するパターンを指定します。

// Swift

let configuration = PURLoggerConfiguration.defaultConfiguration()
configuration.filterSettings = [
    PURFilterSetting(filter: ActivityFilter.self, tagPattern: "activity.**"),
    // filter settings ...
]
configuration.outputSettings = [
    PUROutputSetting(output: ConsoleOutput.self,   tagPattern: "activity.**"),
    PUROutputSetting(output: ConsoleOutput.self,   tagPattern: "pv.**"),
    PUROutputSetting(output: LogServerOutput.self, tagPattern: "pv.**", settings:[PURBufferedOutputSettingsFlushIntervalKey: 10]),
    // output settings ...
]

let logger = PURLogger(configuration: configuration)
// Objective-C

PURLoggerConfiguration *configuration = [PURLoggerConfiguration defaultConfiguration];
configuration.filterSettings = @[
    [[PURFilterSetting alloc] initWithFilter:[ActivityFilter class]
                                  tagPattern:@"activity.**"],
    // filter settings ...
];
configuration.outputSettings = @[
    [[PUROutputSetting alloc] initWithOutput:[ConsoleOutput class]
                                  tagPattern:@"activity.**"],
    [[PUROutputSetting alloc] initWithOutput:[ConsoleOutput class]
                                  tagPattern:@"pv.**"],
    [[PUROutputSetting alloc] initWithOutput:[LogServerOutput class]
                                  tagPattern:@"pv.**"
                                  settings:@{PURBufferedOutputSettingsFlushIntervalKey: @10}],
    // output settings ...
];

PURLogger *logger = [[PURLogger alloc] initWithConfiguration:configuration];

ログを送る

任意のタイミングでログを送ります。 ログにはNSDictionaryなど任意のオブジェクトを指定できます。 フィルタプラグインが受け取ったオブジェクトを加工します。 ログを送るときにはタグを指定します。Pureeは、このタグをもとにどのフィルタまたはアウトプットプラグインを適用するか決定します。

// Swift

logger.postLog(["recipe_id": "123"], tag: "pv.recipe_detail")
// Objective-C

[logger postLog:@{@"recipe_id": "123"} tag: @"pv.recipe_detail"]

タグ

タグには . で区切られたひとつ以上の項で構成される任意の文字列が使えます。例えば、activity.recipe.view, pv.recipe_detail などの文字列が使えます。

プラグインの設定時に渡すタグのパターンには、完全一致のほかにワイルドカード*, **が使えます。

ワイルドカード * はひとつの項に一致します。例えば、aaa.* というパターンは aaa.bbb, aaa.ccc というタグに一致します。aaaaaa.bbb.ccc には一致しません。

ワイルドカード ** は0以上の項に一致します。例えば、aaa.** というパターンは aaaaaa.bbbaaa.bbb.ccc などのタグに一致します。xxx.yyy.zzz には一致しません。

これらのタグのルールは fluentd を参考にしています。ただし {aaa, bbb, ccc} のような項の一部が指定したどれかに一致したらというようなパターンは実装していません(必要だとは思っています)。

フィルタプラグインの定義

フィルタプラグインの役割はロガーが受け取った任意のオブジェクトから Puree のログの内部表現である PURLog インスタンスを生成することです。 PURLog はタグとログの日付、任意の情報を詰められる userInfo プロパティを持ちます。NSCoding プロトコルに準拠していればカスタムクラスのオブジェクトも PURLog に含められます。

フィルタプラグインは PURFilter クラスを継承して実装します。logsWithObject:tag:captured: メソッドを実装して、ひとつまたは複数の PURLog インスタンスを返すようにします。

以下は Recipe や BargainItem というカスタムクラスのオブジェクトから必要な情報をとりだして PURLog のインスタンスを作るフィルタです。

// Swift

class ActivityFilter: PURFilter {
    override func configure(settings: [NSObject : AnyObject]!) {
        super.configure(settings)

        // configure filter plugin
    }

    override func logsWithObject(object: AnyObject!, tag: String!, captured: String!) -> [AnyObject]! {
        let currentDate = self.logger.currentDate()

        if let recipe = object as? Recipe {
            return [PURLog(tag: tag, date: currentDate, userInfo: ["recipe_id": recipe.identifier, "recipe_title": recipe.title])]
        } else if let bargainItem = object as? BargainItem {
            return [PURLog(tag: tag, date: currentDate, userInfo: ["item_id": bargainItem.identifier, "item_name": bargainItem.name])]
        }

        return nil;
    }
}
// Objective-C

#import <Puree.h>

@interface ActivityFilter : PURFilter

@end

@implementation ActivityFilter

- (void)configure:(NSDictionary *)settings
{
    [super configure:settings];

    // configure filter plugin
}

- (NSArray *)logsWithObject:(id)object tag:(NSString *)tag captured:(NSString *)captured
{
    NSDate *currentDate = [self.logger currentDate];

    if ([object isKindOfClass:[Recipe class]]) {
        Recipe *recipe = object;
        return @[[[PURLog alloc] initWithTag:tag date:currentDate userInfo:@{@"recipe_id": recipe.identifier, @"recipe_title": recipe.title}]];
    } else if ([object isKindOfClass:[BargainItem class]]) {
        BargainItem *bargainItem = object;
        return @[[[PURLog alloc] initWithTag:tag date:currentDate userInfo:@{@"item_id": bargainItem.identifier, @"item_name": bargainItem.title}]];
    }

    return nil;
}

アウトプットプラグインの定義

アウトプットプラグインには OutputBufferedOutput の2種類のプラグインがあります。前者はすぐにログの出力を行い、後者はバッファリングと失敗時のリトライを行います。

Output プラグイン

f:id:Slightair:20141223165621p:plain

Output プラグインは、コンソールへの出力やバッファリングなどが考慮された他のログライブラリへ記録するときに向いています。 Output プラグインは PUROutput クラスを継承して定義します。emitLog: メソッドをオーバーライドして出力処理を実装します。

// Swift

class ConsoleOutput: PUROutput {
    override func configure(settings: [NSObject : AnyObject]!) {
        super.configure(settings)

        // configure output plugin
    }

    override func emitLog(log: PURLog!) {
        println("tag: \(log.tag), date: \(log.date), \(log.userInfo)")
    }
}
// Objective-C

#import <Puree.h>

@interface ConsoleOutput : PUROutput

@end

@implementation ConsoleOutput

- (void)configure:(NSDictionary *)settings
{
    [super configure:settings];

    // configure output plugin
}

- (void)emitLog:(PURLog *)log
{
    NSLog(@"tag: %@, date: %@, %@", log.tag, log.date, log.userInfo);
}

Output プラグインには特別なメソッドがあり、特定のイベント時に呼ばれます。

  • start - プラグインが最初に設定されたときに呼ばれる
  • suspend - アプリケーションがバックグランドに入った時に呼ばれる
  • resume - アプリケーションがフォアグラウンドに戻った時に呼ばれる

BufferedOutput プラグイン

f:id:Slightair:20141223165640p:plain

BufferedOutput プラグインはログを外部のサーバに送信する時など一定数のログをまとめたり(バッファリング)、失敗時のリトライが必要になるログ出力に向いています。

BufferedOutput プラグインは PURBufferedOutput クラスを継承して定義します。writeChunk:completion: メソッドをオーバーライドして処理を実装します。Chunk(PURBufferedOutputChunk)は複数のログのコンテナであり、ここからバッファリングされたログを取得できます。completion はその Chunk のログ出力が成功したかどうかを返すハンドラです。ログの出力処理結果に応じてこれを呼ぶ必要があります。もし失敗(fales または NO)とした場合は、一定時間をあけてリトライします。デフォルトでは3回までリトライします。

以下は、ログサーバにログをJSONにシリアライズして送信するプラグインの実装例です。

// Swift

class LogServerOutput: PURBufferedOutput {
    override func configure(settings: [NSObject : AnyObject]!) {
        super.configure(settings)

        // configure buffered output plugin
    }

    override func writeChunk(chunk: PURBufferedOutputChunk!, completion: ((Bool) -> Void)!) {
        let logs = chunk.logs.map { (object: AnyObject) -> NSDictionary in
            let log = object as PURLog
            var logDict = log.userInfo
            logDict["date"] = log.date
            return logDict
        };

        let logData = NSJSONSerialization.dataWithJSONObject(logs, options: nil, error: nil)
        let request = NSURLRequest(URL: NSURL(string:"https://logserver")!)
        let task = NSURLSession.sharedSession().uploadTaskWithRequest(request, fromData: logData, completionHandler:{
            (data:NSData!, response:NSURLResponse!, error:NSError!) -> Void in
            let httpResponse = response as NSHTTPURLResponse
            if error != nil || httpResponse.statusCode != 201 {
                completion(false)
                return
            }
            completion(true)
        })
        task.resume()
    }
}
// Objective-C

#import <Puree.h>

@interface LogServerOutput : PURBufferedOutput

@end

@implementation LogServerOutput

- (void)configure:(NSDictionary *)settings
{
    [super configure:settings];

    // configure buffered output plugin
}

- (void)writeChunk:(PURBufferedOutputChunk *)chunk completion:(void (^)(BOOL))completion
{
    NSMutableArray *logs = [NSMutableArray new];
    for (PURLog *log in chunk.logs) {
        NSMutableDictionary *logDict = [log.userInfo mutableCopy];
        logDict[@"date"] = log.date;

        [logs addObject:logDict];
    }

    NSData *logData = [NSJSONSerialization dataWithJSONObject:logs options:0 error:NULL];
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://logserver"]];
    NSURLSessionUploadTask *task = [[NSURLSession sharedSession] uploadTaskWithRequest:request
                                                                              fromData:logData
                                                                     completionHandler:^(NSData *data, NSURLResponse *response, NSError *error){
                                                                         NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
                                                                         if (error || httpResponse.statusCode != 201) {
                                                                             completion(NO);
                                                                             return;
                                                                         }
                                                                         completion(YES);
                                                                     }];
    [task resume];
}

@end

PURBufferedOutput にはいくつかの設定項目があらかじめ用意されおり、ロガーの初期化時に指定することで動きを変えることができます。

  • PURBufferedOutputSettingsLogLimitKey - バッファリングするログ数(default: 5)
  • PURBufferedOutputSettingsFlushIntervalKey - ログを出力する時間の間隔 (default: 10秒)
  • PURBufferedOutputSettingsMaxRetryCountKey - リトライ回数。この数を超えるとアプリケーションが再起動したりリジュームされるまでリトライしません(default: 3)

まとめ

puree-android に続き、 puree-ios をリリースしました。Puree を使うことでモバイルアプリのログ収集がやりやすくなるとうれしいです。ログを上手に取得することでサービスの改善につなげ、ユーザに快適なアプリを届けられるようになるとうれしいですね。

/* */ @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;*/ /*}*/