How to Add Application Specific Log Data on iOS or OS X

Introduction

For some types of crashes it is helpful to have more data available, than a crash report itself can provide, e.g. application specific log data.

Since the crash report can only be send on the next start, you need to store the log data into a log file. And to make that as fast as possible it should not block the main thread. We highly recommend using CocoaLumberjack or NSLogger or even both in combination using the NSLogger-CocoaLumberjack-connector.

CocoaLumberjack can write log data to multiple destinations non blocking (!!), like the Xcode console or files, and NSLogger has the ability to stream log data over Bonjour to it's Mac application. We do NOT recommend to use NSLog!

Important: Make sure NOT to include personalized data into the log data because of privacy reasons! Also don't send too much data that you will never use. The crash report and the log data should be small in size, so they get send quickly even under bad mobile network conditions.

HowTo

  1. Setup the logging framework of choice
  2. Implement [BITCrashManagerDelegate applicationLogForCrashManager:]
  3. Return the log data

Example

This example code is based on CocoaLumberjack logging into log files using ARC:

Objective-C

    @interface BITAppDelegate () <BITHockeyManagerDelegate> {}
        @property (nonatomic) DDFileLogger *fileLogger;
    @end


    @implementation BITAppDelegate

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
      [self.window makeKeyAndVisible];

      // initialize before HockeySDK, so the delegate can access the file logger!
      _fileLogger = [[DDFileLogger alloc] init];
      _fileLogger.maximumFileSize = (1024 * 64); // 64 KByte
      _fileLogger.logFileManager.maximumNumberOfLogFiles = 1;
      [_fileLogger rollLogFileWithCompletionBlock:nil];
      [DDLog addLogger:_fileLogger];

      [[BITHockeyManager sharedHockeyManager] configureWithIdentifier:@"<>"
                                                         delegate:self];

      [[BITHockeyManager sharedHockeyManager] startManager];

      // add Xcode console logger if not running in the App Store
      if (![[BITHockeyManager sharedHockeyManager] isAppStoreEnvironment]) {
        PSDDFormatter *psLogger = [[PSDDFormatter alloc] init];
        [[DDTTYLogger sharedInstance] setLogFormatter:psLogger];

        [DDLog addLogger:[DDTTYLogger sharedInstance]];

        [DDLog addLogger:[DDNSLoggerLogger sharedInstance]];
      }

      return YES;
    }

    // get the log content with a maximum byte size
    - (NSString *) getLogFilesContentWithMaxSize:(NSInteger)maxSize {
      NSMutableString *description = [NSMutableString string];

      NSArray *sortedLogFileInfos = [[_fileLogger logFileManager] sortedLogFileInfos];
      NSInteger count = [sortedLogFileInfos count];

      // we start from the last one
      for (NSInteger index = count - 1; index >= 0; index--) {
        DDLogFileInfo *logFileInfo = [sortedLogFileInfos objectAtIndex:index];

        NSData *logData = [[NSFileManager defaultManager] contentsAtPath:[logFileInfo filePath]];
        if ([logData length] > 0) {
          NSString *result = [[NSString alloc] initWithBytes:[logData bytes]
                                                      length:[logData length]
                                                    encoding: NSUTF8StringEncoding];

          [description appendString:result];
        }
      }

      if ([description length] > maxSize) {
        description = (NSMutableString *)[description substringWithRange:NSMakeRange([description length]-maxSize-1, maxSize)]; 
      }

      return description;
    }

    #pragma mark - BITCrashManagerDelegate

    - (NSString *)applicationLogForCrashManager:(BITCrashManager *)crashManager {
      NSString *description = [self getLogFilesContentWithMaxSize:5000]; // 5000 bytes should be enough!
      if ([description length] == 0) {
        return nil;
      } else {
        return description;
      }
    }

    @end

Swift

  @UIApplicationMain
  class AppDelegate: UIResponder, UIApplicationDelegate, BITHockeyManagerDelegate {

    var window: UIWindow?
    var fileLogger:DDFileLogger = DDFileLogger()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
      window?.makeKeyAndVisible()
      fileLogger.maximumFileSize = (1024 * 64); // 64 KByte
      fileLogger.logFileManager.maximumNumberOfLogFiles = 1;
      fileLogger.rollLogFile(withCompletion: nil)
      DDLog.add(fileLogger)

      BITHockeyManager.shared().configure(withIdentifier: "APP_IDENTIFIER", delegate: self)
      BITHockeyManager.shared().logLevel = BITLogLevel.verbose
      BITHockeyManager.shared().start()

      return true
    }

    func getLogFilesContentWithMaxSize(_ maxSize: Int) -> String {
      var description = ""
      if let sortedLogFileInfos = fileLogger.logFileManager.sortedLogFileInfos {
        for logFile in sortedLogFileInfos {
          if let logData = FileManager.default.contents(atPath: logFile.filePath) {
            if logData.count > 0 {
              description.append(String(data: logData, encoding: String.Encoding.utf8)!)
            }
          }
        }
      }
      if (description.characters.count > maxSize) {
        description = description.substring(from: description.index(description.startIndex, offsetBy: description.characters.count - maxSize - 1))
      }
      return description;
    }

    func applicationLog(for crashManager: BITCrashManager!) -> String! {
      let description = getLogFilesContentWithMaxSize(5000) // 5000 bytes should be enough!
      if (description.characters.count == 0) {
        return nil
      } else {
        return description
      }
    }
  }