CocoaLumberjackで複数のログファイルに出力分けできるマクロを作ってみた

2014.08.13

CocoaLumberjackとは

CocoaLumberjackについては弊社平井が記事(タグ:CocoaLumberjack)にまとめていますので、ご覧下さい。

以前、【DeNAのiOSエンジニア内で利用頻度の高いライブラリをランキング化してみました #iOS #DeNA】の記事でも紹介された通り、CocoaLumberjackはiOS開発において、高いパフォーマンスを発揮してくれるOSSのログライブラリです。

CocoaLumberjackの導入については今回触れませんので、iOSで使える柔軟なログフレームワーク〜CocoaLumberjackを参考にして下さい。

NSLogが抱える問題

NSLogは非常に簡単に使えます。しかし、アプリに合わせたログ設定に変えたいと思うと途端にCのプリプロセッサ(マクロ)を書かないといけなくなります。

さらに、そのプリプロセッサで複雑な(可読性を損ねるような)処理を書きたくなければ、アプリ側で個々に実装していくしか方法がありません。

小さなプロジェクトでログを出力する場合、NSLogを使用しても特に問題はありません。けれども、以下のような場合は少し面倒になります。

  1. リリースビルドとデバッグビルドでログ出力の設定を変えたい。
  2. ログ出力レベルを設けたい。デバッグ時はエラーログと通信のログのみを出し、リリース時はエラーログのみ出力したい。
  3. ターミナルなど、Xcodeのコンソール以外にもログを出力したい。
  4. パフォーマンスが求められる処理を行いたい時、NSLogが思っていたよりも遅い。けれどもログは出力したい。

Test Flightなどのβ版アプリ配信サービスを使っていると、不具合が起きたときのエビデンスが欲しくなるものです。

β版をインストールしたお客様がWindows環境しか持ち合わせていない場合などは、特にそれに当てはまるでしょう。

「あの画面で何かしたらバグが起こって、アプリがクラッシュしたよ!よくわからないけど今すぐに直して!」と注文をされても、自分の環境で再現するまでに時間がかかり、生産性が非常に悪いです。

CocoaLumberjackを使うと何が良いのか?

GitHubのREADMEを引用します。

Lumberjackの特徴は早くてシンプル、けれども豊富で柔軟な機能がある。

  • NSLogよりも大体の場合は早い。
  • 1つ、ログを出力する処理をかけば、ファイルやコンソールに同時に出力することもできる。つまり、出力先が複数設定できる。
  • 独自のロガーを設定する事も難しくない。ネットワーク経由でファイルシステムにログファイルを保存したり、様々な使い方ができる。
  • ログ出力レベルを設定することができる。実行時、動的にログレベルを変えることもできる。上手く使えば望んだログだけを出せるようになる。

と、非常に優れていて使いやすそうな印象です。

CocoaLumberjackで複数のログファイルにログを残す

今回の本題です。仮定として、

  • システムのログをファイルに残す
  • 位置情報のログをファイルに残す
  • エラーのログをファイルに残す

以上を満たせる実装を行います。

ファイルは全てApplicationディレクトリのDocuments下に生成します。

シミュレータならFinderから、実機ならiTunesなどに繋いで確認が行えます。

実装

Supporting Filesグループ内のPrefix.pchファイルで"DDFileLogger.h"をインポートします。

また、仮定に合ったログ出力レベルと、ログ出力プリプロセッサも末尾に定義します。

SampleLumberjack-Prefix.pch

#import "DDFileLogger.h"

...

/**
 *  CocoaLumberjackの出力レベルを定義します。
 */
static const int ddLogLevel = LOG_LEVEL_ALL;

/**
 *  カスタムログを定義します。
 */
#define LogOutSystemFile(frmt, ...)     ASYNC_LOG_OBJC_MAYBE(ddLogLevel, LOG_FLAG_VERBOSE, 0, frmt, ##__VA_ARGS__)
#define LogOutLocationFile(frmt, ...)   ASYNC_LOG_OBJC_MAYBE(ddLogLevel, LOG_FLAG_INFO, 0, frmt, ##__VA_ARGS__)
#define LogOutErrorFile(frmt, ...)      ASYNC_LOG_OBJC_MAYBE(ddLogLevel, LOG_FLAG_ERROR, 0, frmt, ##__VA_ARGS__)

それぞれのログのフォーマット基底クラスを作成します。

LogFileFormatter.h

#import <Foundation/Foundation.h>

/**
 *  ログファイルフォーマットの基底クラスです。
 *  DDLogFormatterプロトコルに準拠しています。
 *  出力するログファイルのフォーマットを定義します。
 */
@interface LogFileFormatter : NSObject <DDLogFormatter>

@property (nonatomic, assign) NSInteger loggerCount;
@property (nonatomic, strong) NSDateFormatter *threadUnsafeDateFormatter;

@end

LogFileFormatter.m

#import "LogFileFormatter.h"

@implementation LogFileFormatter

- (id)init
{
    self = [super init];
    if(self) {
        self.threadUnsafeDateFormatter = [NSDateFormatter new];
        [self.threadUnsafeDateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
        [self.threadUnsafeDateFormatter setDateFormat:@"yyyy/MM/dd HH:mm:ss:SSS"];
    }
    return self;
}

- (NSString *)formatLogMessage:(DDLogMessage *)logMessage
{
    // 出力するログメッセージは各サブクラスでオーバーライドされます。
    return nil;
}

- (void)didAddToLogger:(id <DDLogger>)logger
{
    self.loggerCount++;
    NSAssert(self.loggerCount <= 1, @"This logger isn't thread-safe");
}

- (void)willRemoveFromLogger:(id <DDLogger>)logger
{
    self.loggerCount--;
}

@end

それぞれのログフォーマットに合ったサブクラスを作成します。

指定のフォーマットがある場合はreturnのNSStringのフォーマットを変更して下さい。今回はログのタイムスタンプを付与しています。

システムログ

LogFileSystemFormatter.h

#import "LogFileFormatter.h"

/**
 *  システムログのフォーマットクラスです。
 */
@interface LogFileSystemFormatter : LogFileFormatter

@end

LogFileSystemFormatter.m

#import "LogFileSystemFormatter.h"

@implementation LogFileSystemFormatter

/**
 *  LOG_LEVEL_VERBOSE以下のレベルのログを全て出力します。
 *
 *  @param DDLogMessageインスタンス
 *
 *  @return ログファイルへ出力する文字列
 */
- (NSString *)formatLogMessage:(DDLogMessage *)logMessage
{
    if (logMessage->logLevel & LOG_LEVEL_VERBOSE) {
        NSString *dateAndTime = [self.threadUnsafeDateFormatter stringFromDate:(logMessage->timestamp)];
        NSString *logMsg = logMessage->logMsg;

        return [NSString stringWithFormat:@"%@ | %@\n", dateAndTime, logMsg];
    }

    return nil;
}

@end

位置情報ログ

LogFileLocationFormatter.h

#import "LogFileFormatter.h"

/**
 *  位置情報ログのフォーマットクラスです。
 */
@interface LogFileLocationFormatter : LogFileFormatter

@end

LogFileLocationFormatter.m

@implementation LogFileLocationFormatter

/**
 *  logMessage->logFlagがLOG_FLAG_INFOのログを全て出力します。
 *
 *  @param DDLogMessageインスタンス
 *
 *  @return ログファイルへ出力する文字列
 */
- (NSString *)formatLogMessage:(DDLogMessage *)logMessage
{
    if (logMessage->logFlag == LOG_FLAG_INFO) {
        NSString *dateAndTime = [self.threadUnsafeDateFormatter stringFromDate:(logMessage->timestamp)];
        NSString *logMsg = logMessage->logMsg;

        return [NSString stringWithFormat:@"%@ | %@\n", dateAndTime, logMsg];
    }

    return nil;
}

@end

エラーログ

LogFileErrorFormatter.h

#import "LogFileFormatter.h"

/**
 *  エラーログのフォーマットクラスです。
 */
@interface LogFileErrorFormatter : LogFileFormatter

@end

LogFileErrorFormatter.m

#import "LogFileErrorFormatter.h"

@implementation LogFileErrorFormatter

/**
 *  logMessage->logFlagがLOG_FLAG_ERRORのログを全て出力します。
 *
 *  @param DDLogMessageインスタンス
 *
 *  @return ログファイルへ出力する文字列
 */
- (NSString *)formatLogMessage:(DDLogMessage *)logMessage
{
    if (logMessage->logFlag == LOG_FLAG_ERROR) {
        NSString *dateAndTime = [self.threadUnsafeDateFormatter stringFromDate:(logMessage->timestamp)];
        NSString *logMsg = logMessage->logMsg;

        return [NSString stringWithFormat:@"%@ | %@\n", dateAndTime, logMsg];
    }

    return nil;
}

@end

以上4クラス(基底フォーマットクラス1つとサブクラス3つ)が今回の

  • システムのログをファイルに残す
  • 位置情報のログをファイルに残す
  • エラーのログをファイルに残す

を実現するフォーマットクラスとなります。そして、フォーマットを生成するクラスを作成します。

LogFileFormatterCreator.h

#import "LogFileFormatter.h"

/**
 *  ログファイルフォーマットを生成するクラスです。
 */
@interface LogFileFormatterCreator : NSObject

/**
 *  システムログのフォーマットを返却します。
 *
 *  @return システムログフォーマット
 */
+ (LogFileFormatter *)createLogFileSystemFormatter;

/**
 *  位置情報ログのフォーマットを返却します。
 *
 *  @return 位置情報ログフォーマット
 */
+ (LogFileFormatter *)createLogFileLocationFormatter;

/**
 *  エラーログのフォーマットを返却します。
 *
 *  @return エラーログフォーマット
 */
+ (LogFileFormatter *)createLogFileErrorFormatter;

@end

LogFileFormatterCreator.m

#import "LogFileFormatterCreator.h"
#import "LogFileSystemFormatter.h"
#import "LogFileLocationFormatter.h"
#import "LogFileErrorFormatter.h"

@implementation LogFileFormatterCreator

+ (LogFileFormatter *)createLogFileSystemFormatter
{
    return [LogFileSystemFormatter new];
}

+ (LogFileFormatter *)createLogFileLocationFormatter
{
    return [LogFileLocationFormatter new];
}

+ (LogFileFormatter *)createLogFileErrorFormatter
{
    return [LogFileErrorFormatter new];
}

@end

このクラスのクラスメソッドを介して各フォーマットのインスタンスを生成します。

DDFileLoggerの初期設定をAppDelegateで呼び出します。

AppDelegate.m

#import "AppDelegate.h"
#import "LogFileFormatterCreator.h"

@implementation AppDelegate

#pragma mark - Application Lifecycle methods

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Override point for customization after application launch.
    [self logFileSettings];
    return YES;
}

...

#pragma mark - DDLogFile Settings methods

/**
 *  ログファイルの出力設定
 */
- (void)logFileSettings
{
    // ログファイル出力先のベースパス指定
    // Documentsディレクトリ下にログファイルが保存されます。
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
                                                         NSUserDomainMask,
                                                         YES);
    NSString *documentPathStr = paths[0];
    // ログファイル出力先ディレクトリの指定
    NSString *filePathStrForFileSystem = [documentPathStr stringByAppendingString:@"/log-System"];
    NSString *filePathStrForFileLocation = [documentPathStr stringByAppendingString:@"/log-Location"];
    NSString *filePathStrForFileError = [documentPathStr stringByAppendingString:@"/log-Error"];

    DDLogFileManagerDefault *fileSystemLogManager = [[DDLogFileManagerDefault alloc] initWithLogsDirectory:filePathStrForFileSystem];
    DDLogFileManagerDefault *fileLocationLogManager = [[DDLogFileManagerDefault alloc] initWithLogsDirectory:filePathStrForFileLocation];
    DDLogFileManagerDefault *fileErrorLogManager = [[DDLogFileManagerDefault alloc] initWithLogsDirectory:filePathStrForFileError];

    [self fileManagerSettings:fileSystemLogManager formatter:[LogFileFormatterCreator createLogFileSystemFormatter]];
    [self fileManagerSettings:fileLocationLogManager formatter:[LogFileFormatterCreator createLogFileLocationFormatter]];
    [self fileManagerSettings:fileErrorLogManager formatter:[LogFileFormatterCreator createLogFileErrorFormatter]];
}

/**
 *  ログファイル出力フォーマットの設定
 *
 *  @param manager       ログファイル出力マネージャー
 *  @param fileFormatter ログファイル出力フォーマット
 */
- (void)fileManagerSettings:(DDLogFileManagerDefault *)manager
                  formatter:(LogFileFormatter *)fileFormatter
{
    DDFileLogger *fileLogger = [[DDFileLogger alloc] initWithLogFileManager:manager];
    // 1MByteまで出力可能
    fileLogger.maximumFileSize = DEFAULT_LOG_MAX_FILE_SIZE;
    // ログファイルのサイズが1MByteを超えた場合、5個まで新しいログファイルが生成できます。
    fileLogger.logFileManager.maximumNumberOfLogFiles = DEFAULT_LOG_MAX_NUM_LOG_FILES;
    fileLogger.logFormatter = fileFormatter;

    [DDLog addLogger:fileLogger];
}

ここまで作れば、後は任意の箇所でカスタムログマクロを呼び出せば使えます。

以下のように使って下さい。

AppDelegate.m

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Override point for customization after application launch.
    [self logFileSettings];

    LogOutSystemFile(@"%s", __func__);
    LogOutLocationFile(@"緯度 = 35.698683, 経度 = 139.774219");
    LogOutErrorFile(@"%sメソッドでエラーが発生しました。\nエラー詳細 ...", __func__);

    return YES;
}

Finder経由でログファイルを確認する(シミュレーター専用)

アプリを実行すると、iPhone Retina(4-inch 64-bit)シミュレーターだと

~/Library/Application\ Support/iPhone\ Simulator/7.1-64/Applications/アプリのID/Documents

以下に3つのディレクトリが作成されます。

スクリーンショット 2014-08-13 14.53.30

  • log-Error
    • LogOutErrorFileで出力した内容が記載されます。(DDLogErrorも含む)
  • log-Location
    • LogOutLocationFileで出力した内容が記載されます。(DDLogInfoも含む)
  • log-System
    • CocoaLumberjackで出力した全てのログが記載されます。

iTunes経由でログファイルを確認する(実機専用)

Info.plistに以下の行を追加します。

Application supports iTunes file sharing : YES

あとはMacでもWindowsでもiTunesに繋げばファイルが確認できます。

スクリーンショット 2014-08-13 15.52.04

任意のフォーマットを追加する

以下の手順で追加できます。

  1. LogFileFormatterを継承したクラスで- (NSString )formatLogMessage:(DDLogMessage )logMessageメソッドをオーバーライドし、任意のフォーマット形式を作成します。
  2. LogFileFormatterCreatorで追加したクラスをインスタンス化して返却するクラスメソッドを追加します。
  3. AppDelegate.mで保存先のディレクトリの指定・DDLogFileManagerDefaultの追加、そしてfileManagerSettingsメソッドの引数に作成したLogFileFormatterを渡します。
  4. カスタムのプリプロセッサを追加します。

出力対象のddLogLevelLOG_FLAG_XXXが足りないようでしたらDDLog.hをインポートしたヘッダークラスに追加で定義し、DDLog.hの代わりにそのクラスをPrefix.pchなど任意の箇所でインポートすると良いです。

まとめ

今回はCocoaLumberjackで複数のログファイルを出力する実装を行いました。

細かくログファイルを分けて、エビデンスを残したい時などは有効に利用できるかと思います。

また、実機でXcode経由で実行していない時にもログファイルが残るので、バグの再現や原因究明などが早く行えます。

CocoaLumberjackをお使いの方は是非お試し下さい。