うな(。・ε・。)

Android, iOS, AppEngine まわりのめもめも

デバッグ用に、ログと CoreData のダンプを送信する

さいきん、デバッグ用に、ログと CoreData のダンプを送信出来る機能を実装しました。

デバッグ情報の送信機能を実装することで、クラッシュレポートでは検知出来ないようなロジックのバグやデータの不整合、その他様々な不具合のデバッグをサポートする事が出来ます。 今までは、このような不具合に対するデバッグを行なう際、データベースとログの内容を見る事が出来ないので非常に歯がゆい思いをしていました。

上記のような、モバイルアプリデバッグの難しさを減らすことを目的とし、表題の機能を実装しました。

CoreData のダンプを送る

NSPersistentStore の永続先として設定してある .sqlite をそのままアップロードしてやれば OK です。

私は MagicalRecord を使っていたので少し工夫が必要でした。 MagicalRecord では、特別なことをしていない場合、次のようにして .sqlite の filepath を取得する事が出来ます。

NSURL *filePath = [NSPersistentStore MR_urlForStoreName:[MagicalRecord MR_defaultStoreName]];

これを AFNetworking とかで適当にアップロードしてやれば OK です。

[self.httpOperationManager POST:url parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
    [formData appendPartWithFileURL:filePath name:@"database" fileName:@"film-model.sqlite" mimeType:@"application/x-sqlite3" error:nil];
} success:^(AFHTTPRequestOperation *operation, id responseObject) {
    FLMLog(@"Debug: Sending dump succeed!");
    
    success(responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    FLMLog(@"Debug: Sending dump failed!");
    
    failure(error);
}];

ログを送信する

CocoaLumberjack/CocoaLumberjack · GitHub を使います。

CocoaLumberjack は、ログを複数のロガーに流せたりするナイスな古参ライブラリです。CocoaLumberjack の FileLogger を使って送信用ログをファイルに貯めます。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [DDLog addLogger:[DDTTYLogger sharedInstance]]; // ふつうのロガー
    DDFileLogger *fileLogger = [[DDFileLogger alloc] init];
    [DDLog addLogger:fileLogger]; // ファイルに書き込むロガー。遅くない。

    // ...
}

初期化時にこのようにロガーの設定を行ないます。

以降は DDLogDebug() などのマクロでログを行なう事が出来ます。 そのため、NSLog を直接使っている場合は、DDLogDebug などに書き換える必要があります。

面倒ですが、どうせ NSLog を直接使うと「プロダクション時にログを吐かない」などの調整が出来ないのでやめましょう。

ログファイルのアップロード

ログファイルの取得には一工夫必要です。なぜなら、DDFileLogger はログローテーションに対応しているため、ログファイルが一つに定まりません。

対象のログ全てを送ればよいのですが、面倒くさいです。最新のログを送っておけば大体 OK です。たぶん。そのためには次のようなハックが必要になります。

ios - Where is Logfile stored using cocoaLumberjack - Stack Overflow

// @see http://stackoverflow.com/questions/6411549/where-is-logfile-stored-using-cocoalumberjack
// CocoaLumberjack の FileLogger から現在の .log ファイルを取得する為のハック
@interface DDFileLogger (CurrentLogFileHack)
- (DDLogFileInfo *)currentLogFileInfo;
@end
//

このようにすれば、fileLogger.currentLogFileInfo.filePath でファイルパスを取得する事が出来ます。

この filePath からファイル用の NSURL を得るには次のようにします。

- (NSURL *)logFileURLForCurrentLogFilePath:(NSString *)filePath {
    // @see http://stackoverflow.com/questions/11798537/reading-a-file-from-documents-directory-objective-c
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *filename = [filePath componentsSeparatedByString:@"/"].lastObject;
    NSString *logFilePath = [documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"/Logs/%@", filename]];
    
    NSURL *fileURL = [NSURL fileURLWithPath:logFilePath];
    
    return fileURL;
}

このようにして取得した FileURL を AFNetworking 等を用いてアップロードしてやります。

[self.httpOperationManager POST:url parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
    [formData appendPartWithFileURL:fileURL name:@"log" fileName:@"film-model.log" mimeType:@"text/plain" error:nil];
} success:^(AFHTTPRequestOperation *operation, id responseObject) {
    FLMLog(@"Debug: Sending log succeed!");
    
    success(responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    FLMLog(@"Debug: Sending log failed!");
    
    failure(error);
}];

AFNetworking の部分はバラバラに書きましたが、実際は一緒に送ります。

以上。

おち:手に入る情報が増えて遥かにマシになりました

デバッグでは情報が多い事がとにかく大事で、何はともあれまずはログとデータです。ログとデータがあればどんな事が起こったか想像がつきますが、無ければ状況から想像するしかありません。

デバッグに関して、面倒くさい事を考えたくなければとにかく様々な情報をたくさん送信しましょう。(*´ω`*)