71

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?

britzl
  • 10,132
  • 7
  • 41
  • 38
Vov4yk
  • 1,080
  • 1
  • 9
  • 13
  • 1
    You can replace NSLog with another function call using a #define. – Jano Sep 05 '11 at 11:10
  • I tried to follow the top answer below but this just messes with my project and throws a ton of parse issues in NSObjCRuntime.h and all over NSobject etc. – Yaron Mushinsky Jun 03 '15 at 14:58

9 Answers9

94

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.

Stavash
  • 14,244
  • 5
  • 52
  • 80
Jano
  • 62,815
  • 21
  • 164
  • 192
  • You can use the Organizer to get the text file: Select "Devices" - #Your Device# - "Applications". Select your application. You see your file (amongst others) in the "Data files in Sandbox" tree below. Click "Download". You now have the file on your mac, and can rightclick "Show Package Contents" to browse to your text file. – thomers Sep 06 '12 at 11:38
  • Can we create a category on NSLog? – Prasad G Mar 04 '14 at 13:03
  • How can i create a category on the class of NSLog ? If yes, can you please suggest what is the class name where NSLog function present? I tried to create category of NSObjCRuntime using "objective c category file creation template" and it is not allowing me go next. Please suggest me how i override NSLog ? Thanks. – Prasad G Mar 05 '14 at 09:42
  • @PrasadG NSLog is not part of any class. It is a C function. To override it you have to redefine it as I did with #define above. – Jano Mar 05 '14 at 09:46
  • When I try to Log an [object description] use this way, system get error. I don't know why. – Yi Jiang Apr 22 '14 at 03:36
  • I follow this guide and all I get are weird parsing exceptions in NSObjCRuntime.h and all over the project, why is that? – Yaron Mushinsky Jun 03 '15 at 14:58
  • i have created this as above but i am not getting proper NSLog() output I am using this class in swift project using bridging header. can please help me in it. – Hardik Vyas Jan 11 '20 at 07:55
  • demo: https://gist.github.com/yinjimmy/4ccaa3b875980b6c642cf4b07c6a13aa – Jimmy KD Mar 18 '21 at 20:00
89

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.

typeoneerror
  • 55,990
  • 32
  • 132
  • 223
Rafał Sroka
  • 39,540
  • 23
  • 113
  • 143
  • when i use this after run the application i have got more than 1 log files , why it become ? can we not use it for only single file? – Ravi Ojha May 05 '15 at 05:42
  • freopen(pathForLog.cStringUsingEncoding(NSASCIIStringEncoding)!, "a+", stderr) . Instead of stderr we need stdout for all logs to redirect to file. With stderr only error logs would go to file. – Mohammad Sadiq May 30 '16 at 07:50
  • 2
    I think NSLog only writes to stderr. – tboyce12 Jul 25 '16 at 18:51
  • 1
    Can somebody please guide, how to clear this file, once its copied into clipboard, so as to start over for next launch of app. – Usman Oct 05 '18 at 11:31
  • One thing I've seen is that if you use a mix of print and NSLog in Swift then they don't get logged into the file in the correct order (prints seem to come much later or not at all). My preference is usually to use NSLog to get the timestamp info anyway. – CMash Oct 24 '19 at 08:20
  • Can we download this xcappdata file and send it through the mail in app ? – Anand Prakash Mar 10 '20 at 07:43
  • @Usman I believe what you're looking for after you're done with copying to clipboard is fclose (http://www.cplusplus.com/reference/cstdio/fclose/) – Bruno Muniz Mar 29 '20 at 04:22
  • Note: For me, starting with macOS 11 this resulted in all sorts of reading issues of sqlite files in combination with CoreData. – Daniel Apr 07 '21 at 09:32
11

I found the simplest solution to the problem: Logging to a file on the iPhone . 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
Community
  • 1
  • 1
JaakL
  • 4,107
  • 5
  • 24
  • 37
  • 1
    Weird that an answer that posted pretty much exactly the same code as this a couple of weeks later got all the upvotes... – CupawnTae May 10 '17 at 10:20
  • 1
    When running from Xcode, it's easier to log to a file on your Mac's disk. Just log to `"~/console.log"`. Then in the terminal, enter `tail -f ~/console.log` for continually updated output. – Alex Zavatone Aug 29 '19 at 16:48
9

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

Bowdzone
  • 3,827
  • 11
  • 39
  • 52
AnitaD
  • 381
  • 4
  • 3
5

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

Martin S
  • 378
  • 2
  • 10
3

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.

Krishnabhadra
  • 34,169
  • 30
  • 118
  • 167
Vov4yk
  • 1,080
  • 1
  • 9
  • 13
3

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; 
}
Fran Sevillano
  • 8,103
  • 4
  • 31
  • 45
2

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

Alvin George
  • 14,148
  • 92
  • 64
0

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)")
            }
        }
    }
}
Hardy_Germany
  • 1,259
  • 13
  • 19