モバイルファースト室の @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
クラスのインスタンスでフィルタやアウトプットなどのプラグインの設定を行い、ロガーの作成時に渡します。
プラグインの設定にはプラグインのクラスとその設定、反応するログのタグと一致するパターンを指定します。
// Swiftlet 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
というタグに一致します。aaa
や aaa.bbb.ccc
には一致しません。
ワイルドカード **
は0以上の項に一致します。例えば、aaa.**
というパターンは aaa
、 aaa.bbb
、 aaa.bbb.ccc
などのタグに一致します。xxx.yyy.zzz
には一致しません。
これらのタグのルールは fluentd を参考にしています。ただし {aaa, bbb, ccc}
のような項の一部が指定したどれかに一致したらというようなパターンは実装していません(必要だとは思っています)。
フィルタプラグインの定義
フィルタプラグインの役割はロガーが受け取った任意のオブジェクトから Puree のログの内部表現である PURLog
インスタンスを生成することです。
PURLog
はタグとログの日付、任意の情報を詰められる userInfo プロパティを持ちます。NSCoding プロトコルに準拠していればカスタムクラスのオブジェクトも PURLog に含められます。
フィルタプラグインは PURFilter
クラスを継承して実装します。logsWithObject:tag:captured:
メソッドを実装して、ひとつまたは複数の PURLog インスタンスを返すようにします。
以下は Recipe や BargainItem というカスタムクラスのオブジェクトから必要な情報をとりだして PURLog のインスタンスを作るフィルタです。
// Swiftclass ActivityFilter: PURFilter { overridefunc configure(settings: [NSObject : AnyObject]!) { super.configure(settings) // configure filter plugin } overridefunc logsWithObject(object: AnyObject!, tag: String!, captured: String!) -> [AnyObject]! { let currentDate =self.logger.currentDate() iflet recipe = object as? Recipe { return [PURLog(tag: tag, date: currentDate, userInfo: ["recipe_id": recipe.identifier, "recipe_title": recipe.title])] } elseiflet bargainItem = object as? BargainItem { return [PURLog(tag: tag, date: currentDate, userInfo: ["item_id": bargainItem.identifier, "item_name": bargainItem.name])] } returnnil; } }
// 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}]]; } elseif ([object isKindOfClass:[BargainItem class]]) { BargainItem *bargainItem = object; return @[[[PURLog alloc] initWithTag:tag date:currentDate userInfo:@{@"item_id": bargainItem.identifier, @"item_name": bargainItem.title}]]; } returnnil; }
アウトプットプラグインの定義
アウトプットプラグインには Output
と BufferedOutput
の2種類のプラグインがあります。前者はすぐにログの出力を行い、後者はバッファリングと失敗時のリトライを行います。
Output プラグイン
Output プラグインは、コンソールへの出力やバッファリングなどが考慮された他のログライブラリへ記録するときに向いています。
Output プラグインは PUROutput
クラスを継承して定義します。emitLog:
メソッドをオーバーライドして出力処理を実装します。
// Swiftclass ConsoleOutput: PUROutput { overridefunc configure(settings: [NSObject : AnyObject]!) { super.configure(settings) // configure output plugin } overridefunc 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 プラグイン
BufferedOutput プラグインはログを外部のサーバに送信する時など一定数のログをまとめたり(バッファリング)、失敗時のリトライが必要になるログ出力に向いています。
BufferedOutput プラグインは PURBufferedOutput
クラスを継承して定義します。writeChunk:completion:
メソッドをオーバーライドして処理を実装します。Chunk(PURBufferedOutputChunk)は複数のログのコンテナであり、ここからバッファリングされたログを取得できます。completion はその Chunk のログ出力が成功したかどうかを返すハンドラです。ログの出力処理結果に応じてこれを呼ぶ必要があります。もし失敗(fales または NO)とした場合は、一定時間をあけてリトライします。デフォルトでは3回までリトライします。
以下は、ログサーバにログをJSONにシリアライズして送信するプラグインの実装例です。
// Swiftclass LogServerOutput: PURBufferedOutput { overridefunc configure(settings: [NSObject : AnyObject]!) { super.configure(settings) // configure buffered output plugin } overridefunc 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 を使うことでモバイルアプリのログ収集がやりやすくなるとうれしいです。ログを上手に取得することでサービスの改善につなげ、ユーザに快適なアプリを届けられるようになるとうれしいですね。