How to NSLog into a file

IosIphoneObjective C

Ios Problem Overview


Is it possible to write every NSLog not only into console, but into a file too? I want to prepare this without replacing NSLog into someExternalFunctionForLogging.

It will be real problem to replace all NSLog. Maybe there is possibility for parsing data from console or catching messages?

Ios Solutions


Solution 1 - Ios

Option 1: Use ASL

NSLog outputs log to ASL (Apple's version of syslog) and console, meaning it is already writing to a file in your Mac when you use the iPhone simulator. If you want to read it open the application Console.app, and type the name of your application in the filter field. To do the same in your iPhone device, you would need to use the ASL API and do some coding.

Option 2: write to a file

Let's say you are running on the simulator and you don't want to use the Console.app. You can redirect the error stream to a file of your liking using freopen:
freopen([path cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr);
See this explanation and sample project for details.

Or you can override NSLog with a custom function using a macro. Example, add this class to your project:

// file Log.h
#define NSLog(args...) _Log(@"DEBUG ", __FILE__,__LINE__,__PRETTY_FUNCTION__,args);
@interface Log : NSObject
void _Log(NSString *prefix, const char *file, int lineNumber, const char *funcName, NSString *format,...);
@end

// file Log.m
#import "Log.h"
@implementation Log
void _Log(NSString *prefix, const char *file, int lineNumber, const char *funcName, NSString *format,...) {
    va_list ap;
    va_start (ap, format);
    format = [format stringByAppendingString:@"\n"];
    NSString *msg = [[NSString alloc] initWithFormat:[NSString stringWithFormat:@"%@",format] arguments:ap];   
    va_end (ap);
    fprintf(stderr,"%s%50s:%3d - %s",[prefix UTF8String], funcName, lineNumber, [msg UTF8String]);
    [msg release];
}
@end

And import it project wide adding the following to your <application>-Prefix.pch:

#import "Log.h"

Now every call to NSLog will be replaced with your custom function without the need to touch your existing code. However, the function above is only printing to console. To add file output, add this function above _Log:

void append(NSString *msg){
    // get path to Documents/somefile.txt
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *path = [documentsDirectory stringByAppendingPathComponent:@"logfile.txt"];
    // create if needed
    if (![[NSFileManager defaultManager] fileExistsAtPath:path]){
        fprintf(stderr,"Creating file at %s",[path UTF8String]);
        [[NSData data] writeToFile:path atomically:YES];
    } 
    // append
    NSFileHandle *handle = [NSFileHandle fileHandleForWritingAtPath:path];
    [handle truncateFileAtOffset:[handle seekToEndOfFile]];
    [handle writeData:[msg dataUsingEncoding:NSUTF8StringEncoding]];
    [handle closeFile];
}

and add this line below fprintf in the _Log function:

append(msg);

File writing also works in your iPhone device, but the file will be created in a directory inside it, and you won't be able to access unless you add code to send it back to your mac, or show it on a view inside your app, or use iTunes to add the documents directory.

Solution 2 - Ios

There is a far easier approach. Here is the method that redirects NSLog output into a file in application’s Documents folder. This can be useful when you want to test your app outside your development studio, unplugged from your mac.

ObjC:

- (void)redirectLogToDocuments 
{
     NSArray *allPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
     NSString *documentsDirectory = [allPaths objectAtIndex:0];
     NSString *pathForLog = [documentsDirectory stringByAppendingPathComponent:@"yourFile.txt"];

     freopen([pathForLog cStringUsingEncoding:NSASCIIStringEncoding],"a+",stderr);
}

Swift:

// 1. Window > Devices and Simulators
// 2. Select the device
// 3. Select your app and click gear icon
// 4. Download container
// 5. Right click and "view contents"
// 6. Find "yourfile.log" under Downloads
//
// redirectLogToDocuments()

func redirectLogToDocuments() {
  let allPaths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
  let documentsDirectory = allPaths.first!
  let pathForLog = "\(documentsDirectory)/yourfile.log"
  freopen(pathForLog.cString(using: String.Encoding.ascii)!, "a+", stdout)
}

After executing this method all output generated by NSLog (ObjC) or print (Swift) will be forwarded to specified file. To get your saved file open Organizer, browse application’s files and save Application Data somewhere in your file system, than simply browse to Documents folder.

Solution 3 - Ios

I found the simplest solution to the problem: https://stackoverflow.com/questions/202299/logging-to-a-file-on-the-iphone/2103091#2103091 . No need to change any NSLog code or change logger itself, just add these 4 lines to your didFinishLaunchingWithOptions and make sure in your build settings that live release will not have this activated (I added LOG2FILE flag for this).

#ifdef LOG2FILE
 #if TARGET_IPHONE_SIMULATOR == 0
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *logPath = [documentsDirectory stringByAppendingPathComponent:@"console.log"];
    freopen([logPath cStringUsingEncoding:NSASCIIStringEncoding],"a+",stderr);
 #endif
#endif

Solution 4 - Ios

Translated the answer of JaakL to Swift, posting it here in any case someone else needs it as well

Run this code somewhere in your app, from that moment it stores all NSLog() output to a file, in the documents directory.

let docDirectory: NSString = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)[0] as NSString
let logpath = docDirectory.stringByAppendingPathComponent("YourFileName.txt")
freopen(logpath.cStringUsingEncoding(NSASCIIStringEncoding)!, "a+", stderr)

Extra: How to find the log-file with Xcode:
You can simply acces the log from Xcode: Windows > Devices > Choose your app > InfoWheelButton > download container. View the file with finder: click right mouse button on file > show package content > appdata > documents > And there the files are

Solution 5 - Ios

Swift 4 version

let docDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let logpathe = docDirectory.appendingPathComponent("Logerr.txt")
freopen(logpathe.path.cString(using: .ascii)!, "a+", stderr)
let logpatho = docDirectory.appendingPathComponent("Logout.txt")
freopen(logpatho.path.cString(using: .ascii)!, "a+", stdout)

Output from Swift print() will be in stdout

Solution 6 - Ios

Ok! firstly, I want to thank Evan-Mulawski. Here is my solution, maybe it will be helpful for someone:

In AppDelegate I add Function:

void logThis(NSString* Msg, ...)
{	
	NSArray* findingMachine = [Msg componentsSeparatedByString:@"%"];
	NSString* outputString = [NSString stringWithString:[findingMachine objectAtIndex:0]];
	va_list argptr;
	va_start(argptr, Msg);

	for(int i = 1; i < [findingMachine count]; i++) {
		if ([[findingMachine objectAtIndex:i] hasPrefix:@"i"]||[[findingMachine objectAtIndex:i] hasPrefix:@"d"]) {
			int argument = va_arg(argptr, int); /* next Arg */
			outputString = [outputString stringByAppendingFormat:@"%i", argument];		
			NSRange range;
			range.location = 0;
			range.length = 1;
			NSString* tmpStr = [[findingMachine objectAtIndex:i] stringByReplacingCharactersInRange:range withString:@""];
			outputString = [outputString stringByAppendingString:tmpStr];
		}
		else if ([[findingMachine objectAtIndex:i] hasPrefix:@"@"]) {
			id argument = va_arg(argptr, id);
			// add argument and next patr of message	
			outputString = [outputString stringByAppendingFormat:@"%@", argument];
			NSRange range;
			range.location = 0;
			range.length = 1;
			NSString* tmpStr = [[findingMachine objectAtIndex:i] stringByReplacingCharactersInRange:range withString:@""];
			outputString = [outputString stringByAppendingString:tmpStr];
		}
		else if ([[findingMachine objectAtIndex:i] hasPrefix:@"."]) {
			double argument = va_arg(argptr, double);		
			// add argument and next patr of message	
			outputString = [outputString stringByAppendingFormat:@"%f", argument];
			NSRange range;
			range.location = 0;
			range.length = 3;
			NSString* tmpStr = [[findingMachine objectAtIndex:i] stringByReplacingCharactersInRange:range withString:@""];
			outputString = [outputString stringByAppendingString:tmpStr];
		}
		else if ([[findingMachine objectAtIndex:i] hasPrefix:@"f"]) {
			double argument = va_arg(argptr, double);		
			// add argument and next patr of message	
			outputString = [outputString stringByAppendingFormat:@"%f", argument];
			NSRange range;
			range.location = 0;
			range.length = 1;
			NSString* tmpStr = [[findingMachine objectAtIndex:i] stringByReplacingCharactersInRange:range withString:@""];
			outputString = [outputString stringByAppendingString:tmpStr];
		}
		else {
			outputString = [outputString stringByAppendingString:@"%"];
			outputString = [outputString stringByAppendingString:[findingMachine objectAtIndex:i]];
		}
	}
	va_end(argptr);
	NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES);
	NSString *	filePath = [[paths objectAtIndex:0]stringByAppendingPathComponent:@"logFile.txt"];
	NSError* theError = nil;
	NSString * fileString = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:&theError];
	if (theError != nil||[fileString length]==0) {
		fileString = [NSString stringWithString:@""];
	}
	fileString = [fileString stringByAppendingFormat:@"\n%@",outputString];
	if(![fileString writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&theError])
	{
			NSLog(@"Loging problem");
	}
	
	NSLog(@"%@",outputString);
}

and, then use "replace for all" NSLog -> logThis. This code is adapted for my app. It can be expand for different needs.


Thnks for help.

Solution 7 - Ios

This is what I use and works well:

http://parmanoir.com/Redirecting_NSLog_to_a_file

Hope it helps.

I'll just post it here for the sake of the content

- (BOOL)redirectNSLog { 
     // Create log file 
     [@"" writeToFile:@"/NSLog.txt" atomically:YES encoding:NSUTF8StringEncoding error:nil]; 
     id fileHandle = [NSFileHandle fileHandleForWritingAtPath:@"/NSLog.txt"]; 
     if (!fileHandle) return NSLog(@"Opening log failed"), NO; 
     [fileHandle retain];  

     // Redirect stderr 
     int err = dup2([fileHandle fileDescriptor], STDERR_FILENO); 
     if (!err) return NSLog(@"Couldn't redirect stderr"), NO;  return YES; 
}

Solution 8 - Ios

Swift 2.0 :

Add these to Appdelegate didFinishLaunchWithOptions.

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
	var paths: Array = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
	let documentsDirectory: String = paths[0]
	let logPath: String = documentsDirectory.stringByAppendingString("/console.log")

	if (isatty(STDERR_FILENO) == 0)
	{
		freopen(logPath, "a+", stderr)
		freopen(logPath, "a+", stdin)
		freopen(logPath, "a+", stdout)
	}
	print(logPath)

	return true
}

Accessing console.log :

When the log path is printed on Xcode Log Area, select the path, right click, choose Services- Reaveal in Finder and open the file console.log

Solution 9 - Ios

I worked a little bit with the answer of Alvin George.

To keep the log file sizes under control I implemented (quick and dirty) a "10 generations of log files" solution and add a func to delete them later on

Every time the app starts, it will generate a new log file with an index "0". The exiting file(s) will be renamed with an index higher than before. Index "10" will be deleted.

So, each start gives you a new log file, maximum 10 generations

Might not be the most elegant way to do it, but works for me during the last weeks very good, as I need some longtime logging "off the mac"

  // -----------------------------------------------------------------------------------------------------------
  // redirectConsoleToFile()
  //
  // does two things  
  // 1) redirects "stderr", "stdin" and "stdout" to a logfile
  // 2) deals with old/existing files to keep up to 10 generations of the logfiles
  // tested with IOS 9.4 and Swift 2.2
  func redirectConsoleToFile() {

    // Instance of a private filemanager
    let myFileManger = NSFileManager.defaultManager()

    // the path of the documnts directory of the app
    let documentDirectory: String = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first!

    // maximum number of logfiles
    let maxNumberOfLogFiles: Int = 10

    // look if the max number of files already exist
    var logFilePath : String = documentDirectory.stringByAppendingString("/Console\(maxNumberOfLogFiles).log")
    var FlagOldFileNoProblem: Bool = true
    if myFileManger.fileExistsAtPath(logFilePath) == true {

        // yes, max number of files reached, so delete the oldest one
        do {
            try myFileManger.removeItemAtPath(logFilePath)

        } catch let error as NSError {

            // something went wrong
            print("ERROR deleting old logFile \(maxNumberOfLogFiles): \(error.description)")
            FlagOldFileNoProblem = false
        }
    }

    // test, if there was a problem with the old file
    if FlagOldFileNoProblem == true {

        // loop over all possible filenames
        for i in 0 ..< maxNumberOfLogFiles {

            // look, if an old file exists, if so, rename it with an index higher than before
            logFilePath = documentDirectory.stringByAppendingString("/Console\((maxNumberOfLogFiles - 1) - i).log")
            if myFileManger.fileExistsAtPath(logFilePath) == true {

                // there is an old file
                let logFilePathNew = documentDirectory.stringByAppendingString("/WayAndSeeConsole\(maxNumberOfLogFiles - i).log")
                do {

                    // rename it
                    try myFileManger.moveItemAtPath(logFilePath, toPath: logFilePathNew)

                } catch let error as NSError {

                    // something went wrong
                    print("ERROR renaming logFile: (i = \(i)), \(error.description)")
                    FlagOldFileNoProblem = false
                }
            }
        }
    }

    // test, if there was a problem with the old files
    if FlagOldFileNoProblem == true {

        // No problem so far, so try to delete the old file
        logFilePath = documentDirectory.stringByAppendingString("/Console0.log")
        if myFileManger.fileExistsAtPath(logFilePath) == true {

            // yes, it exists, so delete it
            do {
                try myFileManger.removeItemAtPath(logFilePath)

            } catch let error as NSError {

                // something went wrong
                print("ERROR deleting old logFile 0: \(error.description)")
            }
        }
    }

    // even if there was a problem with the files so far, we redirect
    logFilePath = documentDirectory.stringByAppendingString("/Console0.log")

    if (isatty(STDIN_FILENO) == 0) {
        freopen(logFilePath, "a+", stderr)
        freopen(logFilePath, "a+", stdin)
        freopen(logFilePath, "a+", stdout)
        displayDebugString(DEBUG_Others, StringToAdd: "stderr, stdin, stdout redirected to \"\(logFilePath)\"")
    } else {
        displayDebugString(DEBUG_Others, StringToAdd: "stderr, stdin, stdout NOT redirected, STDIN_FILENO = \(STDIN_FILENO)")
    }
}

// -----------------------------------------------------------------------------------------------------------
// cleanupOldConsoleFiles()
//
// delete all old consolfiles
func cleanupOldConsoleFiles() {

    // Instance of a private filemanager
    let myFileManger = NSFileManager.defaultManager()

    // the path of the documnts directory of the app
    let documentDirectory: String = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first!

    // maximum number of logfiles
    let maxNumberOfLogFiles: Int = 10

    // working string
    var logFilePath: String = ""

    // loop over all possible filenames
    for i in 0 ... maxNumberOfLogFiles {

        // look, if an old file exists, if so, rename it with an index higher than before
        logFilePath = documentDirectory.stringByAppendingString("/Console\(i).log")
        if myFileManger.fileExistsAtPath(logFilePath) == true {

            // Yes, file exist, so delete it
            do {
                try myFileManger.removeItemAtPath(logFilePath)
            } catch let error as NSError {

                // something went wrong
                print("ERROR deleting old logFile \"\(i)\": \(error.description)")
            }
        }
    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionVov4ykView Question on Stackoverflow
Solution 1 - IosJanoView Answer on Stackoverflow
Solution 2 - IosRafa de KingView Answer on Stackoverflow
Solution 3 - IosJaakLView Answer on Stackoverflow
Solution 4 - IosAnitaDView Answer on Stackoverflow
Solution 5 - IosMartin SView Answer on Stackoverflow
Solution 6 - IosVov4ykView Answer on Stackoverflow
Solution 7 - IosFran SevillanoView Answer on Stackoverflow
Solution 8 - IosAlvin GeorgeView Answer on Stackoverflow
Solution 9 - IosHardy_GermanyView Answer on Stackoverflow